feat: add display of kanban card attachment image

This commit is contained in:
kolaente 2022-10-02 13:01:29 +02:00
parent eae7cc5a6b
commit 3d88fdaadd
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
4 changed files with 91 additions and 56 deletions

View File

@ -145,6 +145,7 @@ import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment' import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -223,10 +224,8 @@ async function deleteAttachment() {
} }
const attachmentImageBlobUrl = ref<string | null>(null) const attachmentImageBlobUrl = ref<string | null>(null)
const SUPPORTED_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
async function viewOrDownload(attachment: AttachmentModel) { async function viewOrDownload(attachment: AttachmentModel) {
if (SUPPORTED_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)

View File

@ -11,62 +11,70 @@
@click.ctrl="() => toggleTaskDone(task)" @click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)" @click.meta="() => toggleTaskDone(task)"
> >
<span class="task-id"> <img
<Done class="kanban-card__done" :is-done="task.done" variant="small"/> v-if="coverImageBlobUrl"
<template v-if="task.identifier === ''"> :src="coverImageBlobUrl"
#{{ task.index }} alt=""
</template> class="cover-image"
<template v-else> />
{{ task.identifier }} <div class="p-2">
</template> <span class="task-id">
</span> <Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<span <template v-if="task.identifier === ''">
:class="{'overdue': task.dueDate <= new Date() && !task.done}" #{{ task.index }}
class="due-date" </template>
v-if="task.dueDate > 0" <template v-else>
v-tooltip="formatDateLong(task.dueDate)"> {{ task.identifier }}
<span class="icon"> </template>
<icon :icon="['far', 'calendar-alt']"/>
</span> </span>
<time :datetime="formatISO(task.dueDate)"> <span
{{ formatDateSince(task.dueDate) }} :class="{'overdue': task.dueDate <= new Date() && !task.done}"
</time> class="due-date"
</span> v-if="task.dueDate > 0"
<h3>{{ task.title }}</h3> v-tooltip="formatDateLong(task.dueDate)">
<progress <span class="icon">
class="progress is-small" <icon :icon="['far', 'calendar-alt']"/>
v-if="task.percentDone > 0" </span>
:value="task.percentDone * 100" max="100"> <time :datetime="formatISO(task.dueDate)">
{{ task.percentDone * 100 }}% {{ formatDateSince(task.dueDate) }}
</progress> </time>
<div class="footer"> </span>
<labels :labels="task.labels"/> <h3>{{ task.title }}</h3>
<priority-label :priority="task.priority" :done="task.done"/> <progress
<div class="assignees" v-if="task.assignees.length > 0"> class="progress is-small"
<user v-if="task.percentDone > 0"
v-for="u in task.assignees" :value="task.percentDone * 100" max="100">
:avatar-size="24" {{ task.percentDone * 100 }}%
:key="task.id + 'assignee' + u.id" </progress>
:show-username="false" <div class="footer">
:user="u" <labels :labels="task.labels"/>
/> <priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
v-for="u in task.assignees"
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
/>
</div>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<icon icon="align-left"/>
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</div> </div>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<icon icon="align-left"/>
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
@ -77,6 +85,8 @@ import ChecklistSummary from './checklist-summary.vue'
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task' import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
@ -114,6 +124,29 @@ function openTaskDetail() {
state: {backdropView: router.currentRoute.value.fullPath}, state: {backdropView: router.currentRoute.value.fullPath},
}) })
} }
const coverImageBlobUrl = ref<string | null>(null)
async function maybeDownloadCoverImage() {
if (!props.task.coverImageAttachmentId) {
return
}
const attachment = props.task.attachments.find(a => a.id === props.task.coverImageAttachmentId)
if (!attachment || !SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
console.log('not an attachment')
return
}
const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
}
watch(
() => props.task.coverImageAttachmentId,
maybeDownloadCoverImage,
{immediate: true},
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -125,12 +158,11 @@ $task-background: var(--white);
cursor: pointer; cursor: pointer;
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
display: block; display: block;
border: 3px solid transparent;
font-size: .9rem; font-size: .9rem;
padding: .4rem;
border-radius: $radius; border-radius: $radius;
background: $task-background; background: $task-background;
overflow: hidden;
&.loader-container.is-loading::after { &.loader-container.is-loading::after {
width: 1.5rem; width: 1.5rem;

View File

@ -1,4 +1,4 @@
import type { Priority } from '@/constants/priorities' import type {Priority} from '@/constants/priorities'
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser' import type {IUser} from './IUser'
@ -11,6 +11,7 @@ import type {IBucket} from './IBucket'
import type {IRelationKind} from '@/types/IRelationKind' import type {IRelationKind} from '@/types/IRelationKind'
import type {IRepeatAfter} from '@/types/IRepeatAfter' import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {IRepeatMode} from '@/types/IRepeatMode' import type {IRepeatMode} from '@/types/IRepeatMode'
export interface ITask extends IAbstract { export interface ITask extends IAbstract {
id: number id: number
title: string title: string
@ -31,8 +32,9 @@ export interface ITask extends IAbstract {
parentTaskId: ITask['id'] parentTaskId: ITask['id']
hexColor: string hexColor: string
percentDone: number percentDone: number
relatedTasks: Partial<Record<IRelationKind, ITask[]>>, relatedTasks: Partial<Record<IRelationKind, ITask[]>>
attachments: IAttachment[] attachments: IAttachment[]
coverImageAttachmentId: IAttachment['id']
identifier: string identifier: string
index: number index: number
isFavorite: boolean isFavorite: boolean

View File

@ -5,6 +5,8 @@ import type { IUser } from '@/modelTypes/IUser'
import type { IFile } from '@/modelTypes/IFile' import type { IFile } from '@/modelTypes/IFile'
import type { IAttachment } from '@/modelTypes/IAttachment' import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment { export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0 id = 0
taskId = 0 taskId = 0