Skip to content

Commit 97ebe9f

Browse files
Rumburaq2cugu
andauthored
feat: add image preview feature (#1161)
Co-authored-by: Jonas Plum <[email protected]>
1 parent 770390c commit 97ebe9f

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import { Button } from '@/components/ui/button'
3+
4+
import { X } from 'lucide-vue-next'
5+
6+
defineProps<{
7+
modelValue: boolean
8+
imageUrl: string
9+
fileName: string
10+
}>()
11+
12+
const emit = defineEmits(['update:modelValue'])
13+
14+
const closeModal = () => {
15+
emit('update:modelValue', false)
16+
}
17+
</script>
18+
19+
<template>
20+
<div
21+
v-if="modelValue"
22+
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
23+
@click.self="closeModal"
24+
>
25+
<div class="relative rounded-lg bg-white p-2 shadow-xl">
26+
<img :src="imageUrl" :alt="fileName" class="max-h-[80vh] max-w-[80vw]" />
27+
<Button
28+
variant="ghost"
29+
size="icon"
30+
class="absolute -right-1 -top-1 h-8 w-8 rounded-full bg-white"
31+
@click="closeModal"
32+
>
33+
<X class="h-5 w-5" />
34+
<span class="sr-only">Close image modal</span>
35+
</Button>
36+
</div>
37+
</div>
38+
</template>

ui/src/components/ticket/file/TicketFiles.vue

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '@uppy/dashboard/dist/style.min.css'
55
import DeleteDialog from '@/components/common/DeleteDialog.vue'
66
import TicketPanel from '@/components/ticket/TicketPanel.vue'
77
import FileAddDialog from '@/components/ticket/file/FileAddDialog.vue'
8+
import ImageModal from '@/components/ticket/file/ImageModal.vue'
89
import { Button } from '@/components/ui/button'
910
import { useToast } from '@/components/ui/toast/use-toast'
1011
@@ -30,6 +31,36 @@ const props = defineProps<{
3031
files: Array<ModelFile> | undefined
3132
}>()
3233
34+
const isImageModalOpen = ref(false)
35+
const selectedImageUrl = ref('')
36+
const selectedImageName = ref('')
37+
38+
const isImage = (fileName: string) => {
39+
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
40+
const extension = fileName.split('.').pop()?.toLowerCase()
41+
return extension ? imageExtensions.includes(extension) : false
42+
}
43+
44+
const openImageModal = async (file: ModelFile) => {
45+
if (!isImage(file.name)) return
46+
try {
47+
const response = await fetch(`/api/files/${file.id}/download`, {
48+
headers: { Authorization: `Bearer ${authStore.token}` }
49+
})
50+
const blob = await response.blob()
51+
selectedImageUrl.value = window.URL.createObjectURL(blob)
52+
selectedImageName.value = file.name
53+
isImageModalOpen.value = true
54+
} catch (err) {
55+
console.error('Error fetching image:', err)
56+
toast({
57+
title: 'Error',
58+
description: 'Could not load image for preview.',
59+
variant: 'destructive'
60+
})
61+
}
62+
}
63+
3364
const downloadFile = (file: any) => {
3465
fetch(`/api/files/${file.id}/download`, {
3566
headers: { Authorization: `Bearer ${authStore.token}` }
@@ -44,6 +75,8 @@ const downloadFile = (file: any) => {
4475
link.download = file.name
4576
document.body.appendChild(link)
4677
link.click()
78+
document.body.removeChild(link)
79+
window.URL.revokeObjectURL(_url)
4780
})
4881
.catch((err) => {
4982
console.log(err)
@@ -82,11 +115,24 @@ watch(
82115
},
83116
{ immediate: true }
84117
)
118+
119+
watch(isImageModalOpen, (isOpen) => {
120+
if (!isOpen && selectedImageUrl.value) {
121+
window.URL.revokeObjectURL(selectedImageUrl.value)
122+
selectedImageUrl.value = ''
123+
selectedImageName.value = ''
124+
}
125+
})
85126
</script>
86127

87128
<template>
88129
<TicketPanel title="Files" @add="dialogOpen = true" :hideAdd="isDemo">
89130
<FileAddDialog v-if="!isDemo" v-model="dialogOpen" :ticket="ticket" />
131+
<ImageModal
132+
v-model="isImageModalOpen"
133+
:image-url="selectedImageUrl"
134+
:file-name="selectedImageName"
135+
/>
90136
<div
91137
v-if="!files || files.length === 0"
92138
class="flex h-10 items-center p-4 text-muted-foreground"
@@ -99,7 +145,11 @@ watch(
99145
:title="file.name"
100146
class="flex w-full items-center border-t py-1 pl-2 pr-1 first:rounded-t first:border-none last:rounded-b"
101147
>
102-
<div class="flex flex-1 items-center overflow-hidden pr-2">
148+
<div
149+
class="flex flex-1 items-center overflow-hidden pr-2"
150+
:class="isImage(file.name) ? 'cursor-pointer' : ''"
151+
@click="openImageModal(file)"
152+
>
103153
{{ file.name }}
104154

105155
<div class="ml-1 flex-1 text-nowrap text-sm text-muted-foreground">

0 commit comments

Comments
 (0)