feat: promote an attachment to task cover image
This commit is contained in:
parent
054d70cbe5
commit
877e425055
3 changed files with 53 additions and 25 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue