feat: promote an attachment to task cover image

This commit is contained in:
kolaente 2022-10-02 13:40:39 +02:00
parent 054d70cbe5
commit 877e425055
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 53 additions and 25 deletions

View File

@ -9,7 +9,7 @@
<input <input
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading || undefined" :disabled="loading || undefined"
@change="uploadNewAttachment()" @change="uploadNewAttachment()"
id="files" id="files"
multiple multiple
@ -78,6 +78,13 @@
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</BaseButton> </BaseButton>
<BaseButton
v-if="editEnabled"
class="attachment-info-meta-button"
@click.prevent.stop="setCoverImage(task.coverImageAttachmentId === a.id ? null : a)"
>
{{ task.coverImageAttachmentId === a.id ? $t('task.attachment.unsetAsCover') : $t('task.attachment.setAsCover') }}
</BaseButton>
</p> </p>
</div> </div>
</a> </a>
@ -85,7 +92,7 @@
<x-button <x-button
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading" :disabled="loading"
@click="filesRef?.click()" @click="filesRef?.click()"
class="mb-4" class="mb-4"
icon="cloud-upload-alt" icon="cloud-upload-alt"
@ -118,7 +125,7 @@
<template #header> <template #header>
<span>{{ $t('task.attachment.delete') }}</span> <span>{{ $t('task.attachment.delete') }}</span>
</template> </template>
<template #text> <template #text>
<p> <p>
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/> {{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
@ -156,38 +163,44 @@ import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments'
import {getHumanSize} from '@/helpers/getHumanSize' import {getHumanSize} from '@/helpers/getHumanSize'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useI18n} from 'vue-i18n'
const props = defineProps({ const taskStore = useTaskStore()
taskId: { const {t} = useI18n()
type: Number as PropType<ITask['id']>,
required: true, const props = withDefaults(defineProps<{
}, task: ITask,
initialAttachments: { initialAttachments?: IAttachment[],
type: Array, editEnabled: boolean,
}, }>(), {
editEnabled: { editEnabled: true,
default: true,
},
}) })
// FIXME: this should go through the store
const emit = defineEmits(['task-changed'])
const attachmentService = shallowReactive(new AttachmentService()) const attachmentService = shallowReactive(new AttachmentService())
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
const attachments = computed(() => attachmentStore.attachments) const attachments = computed(() => attachmentStore.attachments)
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
function onDrop(files: File[] | null) { function onDrop(files: File[] | null) {
if (files && files.length !== 0) { if (files && files.length !== 0) {
uploadFilesToTask(files) uploadFilesToTask(files)
} }
} }
const { isOverDropZone } = useDropZone(document, onDrop) const {isOverDropZone} = useDropZone(document, onDrop)
function downloadAttachment(attachment: IAttachment) { function downloadAttachment(attachment: IAttachment) {
attachmentService.download(attachment) attachmentService.download(attachment)
} }
const filesRef = ref<HTMLInputElement | null>(null) const filesRef = ref<HTMLInputElement | null>(null)
function uploadNewAttachment() { function uploadNewAttachment() {
const files = filesRef.value?.files const files = filesRef.value?.files
@ -199,7 +212,7 @@ function uploadNewAttachment() {
} }
function uploadFilesToTask(files: File[] | FileList) { function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.taskId, files) uploadFiles(attachmentService, props.task.id, files)
} }
const attachmentToDelete = ref<AttachmentModel | null>(null) const attachmentToDelete = ref<AttachmentModel | null>(null)
@ -218,14 +231,15 @@ async function deleteAttachment() {
attachmentStore.removeById(attachmentToDelete.value.id) attachmentStore.removeById(attachmentToDelete.value.id)
success(r) success(r)
setAttachmentToDelete(null) setAttachmentToDelete(null)
} catch(e) { } catch (e) {
error(e) error(e)
} }
} }
const attachmentImageBlobUrl = ref<string | null>(null) const attachmentImageBlobUrl = ref<string | null>(null)
async function viewOrDownload(attachment: AttachmentModel) { async function viewOrDownload(attachment: AttachmentModel) {
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix)) ) { if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment) attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else { } else {
downloadAttachment(attachment) downloadAttachment(attachment)
@ -233,8 +247,18 @@ async function viewOrDownload(attachment: AttachmentModel) {
} }
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) { function copyUrl(attachment: IAttachment) {
copy(generateAttachmentUrl(props.taskId, attachment.id)) copy(generateAttachmentUrl(props.task.id, attachment.id))
}
async function setCoverImage(attachment: IAttachment | null) {
const task = await taskStore.update({
...props.task,
coverImageAttachmentId: attachment ? attachment.id : 0,
})
emit('task-changed', task)
success({message: t('task.attachment.successfullyChangedCoverImage')})
} }
</script> </script>
@ -315,7 +339,7 @@ function copyUrl(attachment: IAttachment) {
height: auto; height: auto;
text-shadow: var(--shadow-md); text-shadow: var(--shadow-md);
animation: bounce 2s infinite; animation: bounce 2s infinite;
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
animation: none; animation: none;
} }
@ -337,7 +361,7 @@ function copyUrl(attachment: IAttachment) {
.attachment-info-meta { .attachment-info-meta {
display: flex; display: flex;
align-items: center; align-items: center;
:deep(.user) { :deep(.user) {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@ -347,7 +371,7 @@ function copyUrl(attachment: IAttachment) {
@media screen and (max-width: $mobile) { @media screen and (max-width: $mobile) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
:deep(.user) { :deep(.user) {
margin: .5rem 0; margin: .5rem 0;
} }

View File

@ -693,7 +693,10 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Set as cover image",
"unsetAsCover": "Unset as cover image",
"successfullyChangedCoverImage": "The cover image was successfully changed."
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",

View File

@ -218,7 +218,8 @@
<div class="content attachments" v-if="activeFields.attachments || hasAttachments"> <div class="content attachments" v-if="activeFields.attachments || hasAttachments">
<attachments <attachments
:edit-enabled="canWrite" :edit-enabled="canWrite"
:task-id="taskId" :task="task"
@task-changed="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId"
ref="attachments" ref="attachments"
/> />
</div> </div>
@ -500,7 +501,7 @@ const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const kanbanStore = useKanbanStore() const kanbanStore = useKanbanStore()
const task = reactive(new TaskModel()) const task = reactive<ITask>(new TaskModel())
useTitle(toRef(task, 'title')) useTitle(toRef(task, 'title'))
// We doubled the task color property here because verte does not have a real change property, leading // We doubled the task color property here because verte does not have a real change property, leading