From 97ebe9f01a7e79760c21f2c3eef051f7fe19aacc Mon Sep 17 00:00:00 2001 From: Rumburaq2 Date: Sun, 26 Oct 2025 07:38:09 +0100 Subject: [PATCH] feat: add image preview feature (#1161) Co-authored-by: Jonas Plum --- ui/src/components/ticket/file/ImageModal.vue | 38 ++++++++++++++ ui/src/components/ticket/file/TicketFiles.vue | 52 ++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/ticket/file/ImageModal.vue diff --git a/ui/src/components/ticket/file/ImageModal.vue b/ui/src/components/ticket/file/ImageModal.vue new file mode 100644 index 0000000..f6281f5 --- /dev/null +++ b/ui/src/components/ticket/file/ImageModal.vue @@ -0,0 +1,38 @@ + + + diff --git a/ui/src/components/ticket/file/TicketFiles.vue b/ui/src/components/ticket/file/TicketFiles.vue index 8b44672..e1a08aa 100644 --- a/ui/src/components/ticket/file/TicketFiles.vue +++ b/ui/src/components/ticket/file/TicketFiles.vue @@ -5,6 +5,7 @@ import '@uppy/dashboard/dist/style.min.css' import DeleteDialog from '@/components/common/DeleteDialog.vue' import TicketPanel from '@/components/ticket/TicketPanel.vue' import FileAddDialog from '@/components/ticket/file/FileAddDialog.vue' +import ImageModal from '@/components/ticket/file/ImageModal.vue' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/toast/use-toast' @@ -30,6 +31,36 @@ const props = defineProps<{ files: Array | undefined }>() +const isImageModalOpen = ref(false) +const selectedImageUrl = ref('') +const selectedImageName = ref('') + +const isImage = (fileName: string) => { + const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'] + const extension = fileName.split('.').pop()?.toLowerCase() + return extension ? imageExtensions.includes(extension) : false +} + +const openImageModal = async (file: ModelFile) => { + if (!isImage(file.name)) return + try { + const response = await fetch(`/api/files/${file.id}/download`, { + headers: { Authorization: `Bearer ${authStore.token}` } + }) + const blob = await response.blob() + selectedImageUrl.value = window.URL.createObjectURL(blob) + selectedImageName.value = file.name + isImageModalOpen.value = true + } catch (err) { + console.error('Error fetching image:', err) + toast({ + title: 'Error', + description: 'Could not load image for preview.', + variant: 'destructive' + }) + } +} + const downloadFile = (file: any) => { fetch(`/api/files/${file.id}/download`, { headers: { Authorization: `Bearer ${authStore.token}` } @@ -44,6 +75,8 @@ const downloadFile = (file: any) => { link.download = file.name document.body.appendChild(link) link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(_url) }) .catch((err) => { console.log(err) @@ -82,11 +115,24 @@ watch( }, { immediate: true } ) + +watch(isImageModalOpen, (isOpen) => { + if (!isOpen && selectedImageUrl.value) { + window.URL.revokeObjectURL(selectedImageUrl.value) + selectedImageUrl.value = '' + selectedImageName.value = '' + } +})