Frontend: Proof of concept for image preview #2266
|
@ -42,7 +42,7 @@ function uploadAttachmentAndVerify(taskId: number) {
|
|||
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
|
||||
cy.wait('@uploadAttachment')
|
||||
|
||||
cy.get('.attachments .attachments .files a.attachment')
|
||||
cy.get('.attachments .attachments .files button.attachment')
|
||||
|
||||
.should('exist')
|
||||
}
|
||||
|
||||
|
|
|
@ -24,11 +24,15 @@ import {
|
|||
faCocktail,
|
||||
faCoffee,
|
||||
faCog,
|
||||
faCopy,
|
||||
faDownload,
|
||||
faEllipsisH,
|
||||
faEllipsisV,
|
||||
faExclamation,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFile,
|
||||
faFileImage,
|
||||
faFillDrip,
|
||||
faFilter,
|
||||
faForward,
|
||||
|
@ -81,7 +85,6 @@ import {
|
|||
faCheckSquare,
|
||||
faClock,
|
||||
faComments,
|
||||
faFileImage,
|
||||
faSave,
|
||||
faSquareCheck,
|
||||
faStar,
|
||||
|
@ -102,6 +105,7 @@ library.add(faUnlink)
|
|||
library.add(faParagraph)
|
||||
library.add(faSquareCheck)
|
||||
library.add(faTable)
|
||||
library.add(faFile)
|
||||
library.add(faFileImage)
|
||||
library.add(faCheckSquare)
|
||||
library.add(faStrikethrough)
|
||||
|
@ -130,6 +134,8 @@ library.add(faCocktail)
|
|||
library.add(faCoffee)
|
||||
library.add(faCog)
|
||||
library.add(faComments)
|
||||
library.add(faCopy)
|
||||
library.add(faDownload)
|
||||
library.add(faEllipsisH)
|
||||
library.add(faEllipsisV)
|
||||
library.add(faExclamation)
|
||||
|
|
|
@ -27,15 +27,19 @@
|
|||
v-if="attachments.length > 0"
|
||||
class="files"
|
||||
>
|
||||
<!-- FIXME: don't use a for element that wraps other links / buttons
|
||||
Instead: overlay element with button that is inside.
|
||||
-->
|
||||
<a
|
||||
<button
|
||||
konrad
commented
Please do not use a table for multi-column layout. Please do not use a table for multi-column layout.
Elscrux
commented
I used a grid instead now I used a grid instead now
|
||||
v-for="a in attachments"
|
||||
:key="a.id"
|
||||
class="attachment"
|
||||
konrad
commented
`grid-item` sounds like a child of a grid. Why not call it `attachment` as it was before?
|
||||
@click="viewOrDownload(a)"
|
||||
>
|
||||
<div class="preview-column">
|
||||
<FilePreview
|
||||
class="attachment-preview"
|
||||
:model-value="a"
|
||||
/>
|
||||
</div>
|
||||
<div class="attachment-info-column">
|
||||
<div class="filename">
|
||||
{{ a.file.name }}
|
||||
<span
|
||||
|
@ -73,14 +77,14 @@
|
|||
class="attachment-info-meta-button"
|
||||
@click.prevent.stop="downloadAttachment(a)"
|
||||
>
|
||||
{{ $t('misc.download') }}
|
||||
<icon icon="download" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-tooltip="$t('task.attachment.copyUrlTooltip')"
|
||||
class="attachment-info-meta-button"
|
||||
@click.stop="copyUrl(a)"
|
||||
>
|
||||
{{ $t('task.attachment.copyUrl') }}
|
||||
<icon icon="copy" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="editEnabled"
|
||||
|
@ -88,22 +92,22 @@
|
|||
class="attachment-info-meta-button"
|
||||
@click.prevent.stop="setAttachmentToDelete(a)"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
<icon icon="trash-alt" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
konrad
commented
This needs a tooltip This needs a tooltip
|
||||
v-if="editEnabled"
|
||||
v-if="editEnabled && canPreview(a)"
|
||||
v-tooltip="task.coverImageAttachmentId === a.id
|
||||
? $t('task.attachment.unsetAsCover')
|
||||
: $t('task.attachment.setAsCover')"
|
||||
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')
|
||||
}}
|
||||
<icon :icon="task.coverImageAttachmentId === a.id ? 'eye-slash' : 'eye'" />
|
||||
</BaseButton>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
|
@ -188,6 +192,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
|||
import {error, success} from '@/message'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import FilePreview from '@/components/tasks/partials/file-preview.vue'
|
||||
|
||||
const {
|
||||
task,
|
||||
|
@ -260,13 +265,17 @@ async function deleteAttachment() {
|
|||
const attachmentImageBlobUrl = ref<string | null>(null)
|
||||
|
||||
async function viewOrDownload(attachment: IAttachment) {
|
||||
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
|
||||
if (canPreview(attachment)) {
|
||||
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
|
||||
} else {
|
||||
downloadAttachment(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
function canPreview(attachment: IAttachment): boolean {
|
||||
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))
|
||||
}
|
||||
|
||||
const copy = useCopyToClipboard()
|
||||
|
||||
function copyUrl(attachment: IAttachment) {
|
||||
|
@ -298,26 +307,37 @@ async function setCoverImage(attachment: IAttachment | null) {
|
|||
}
|
||||
|
||||
.attachment {
|
||||
konrad
commented
Please make this Please make this `width: 100%`.
Elscrux
commented
done! done!
|
||||
margin-bottom: .5rem;
|
||||
display: block;
|
||||
transition: background-color $transition;
|
||||
border-radius: $radius;
|
||||
display: grid;
|
||||
grid-template-columns: 9rem 1fr;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
padding: .5rem;
|
||||
|
||||
transition: background-color $transition;
|
||||
background-color: transparent;
|
||||
|
||||
border: transparent;
|
||||
border-radius: $radius;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--grey-200);
|
||||
}
|
||||
}
|
||||
|
||||
.filename {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: .25rem;
|
||||
height: 2rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.info {
|
||||
color: var(--grey-500);
|
||||
font-size: .9rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
|
@ -375,6 +395,12 @@ async function setCoverImage(attachment: IAttachment | null) {
|
|||
}
|
||||
}
|
||||
|
||||
.attachment-info-column {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.attachment-info-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -406,6 +432,7 @@ async function setCoverImage(attachment: IAttachment | null) {
|
|||
|
||||
.attachment-info-meta-button {
|
||||
color: var(--link);
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
|
@ -434,9 +461,19 @@ async function setCoverImage(attachment: IAttachment | null) {
|
|||
}
|
||||
}
|
||||
|
||||
.preview-column {
|
||||
max-width: 8rem;
|
||||
height: 5.2rem;
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-task-cover {
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
margin-left: .25rem;
|
||||
padding: .25rem .35rem;
|
||||
border-radius: 4px;
|
||||
font-size: .75rem;
|
||||
|
|
58
frontend/src/components/tasks/partials/file-preview.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<!-- Preview image -->
|
||||
konrad
commented
Can you make this always the same size? Use Can you make this always the same size? Use [`object-fit: cover;`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) to let the image fill the container nicely.
Elscrux
commented
I included it, but it didn't seem like it made a difference I included it, but it didn't seem like it made a difference
konrad
commented
Did you set a fixed with and height for the image? Did you set a fixed with and height for the image?
Elscrux
commented
Yes, it's at 4rem now Yes, it's at 4rem now
|
||||
<img
|
||||
v-if="blobUrl"
|
||||
:src="blobUrl"
|
||||
konrad
commented
Please do not use inline styles. This image should have a rectangular fixed size, then the Please do not use inline styles.
This image should have a rectangular fixed size, then the `object-fit` property will make sure it looks good.
|
||||
alt="Attachment preview"
|
||||
>
|
||||
|
||||
<!-- Fallback -->
|
||||
<div
|
||||
konrad
commented
Can you use a simple file icon here? An image icon suggests the file is an image, which it is not. Can you use a simple [file icon](https://fontawesome.com/icons/file?f=classic&s=regular) here? An image icon suggests the file is an image, which it is not.
konrad
commented
Also please make the file icon in a dark gray, similar to the attachment title. Also please make the file icon in a dark gray, similar to the attachment title.
|
||||
v-else
|
||||
class="icon-wrapper"
|
||||
>
|
||||
<icon
|
||||
size="6x"
|
||||
icon="file"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type PropType, ref, shallowReactive, watchEffect} from 'vue'
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<IAttachment>,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
Please use Please use `$radius` here so that the radius is consistent with other elements.
Elscrux
commented
Done! Done!
|
||||
|
||||
konrad
commented
Adding a Adding a `width: 100%` here will make it look like what I meant (will share a screenshot later)
|
||||
const attachmentService = shallowReactive(new AttachmentService())
|
||||
const blobUrl = ref<string | undefined>(undefined)
|
||||
|
||||
watchEffect(async () => {
|
||||
if (props.modelValue && canPreview(props.modelValue)) {
|
||||
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue)
|
||||
}
|
||||
})
|
||||
|
||||
function canPreview(attachment: IAttachment): boolean {
|
||||
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: $radius;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
color: var(--grey-500);
|
||||
}
|
||||
</style>
|
This needs to be changed again to make the test pass.