Add image preview and download link support for task attachments in Self-Serve components

- Updated `MyWashStart.vue` and `SelfServeTryModal.vue` to fetch and display download links for image attachments.
- Enhanced UI to show inline image previews for attachments with valid download links.
- Introduced `isImage` utility function to identify image formats.
This commit is contained in:
Jeppe Bundgaard
2026-01-19 11:23:42 +01:00
parent 2435a85ff9
commit 3ecaee7aed
2 changed files with 64 additions and 10 deletions
@@ -85,7 +85,22 @@ const fetchData = async () => {
tasks.value = await Promise.all(filteredTasks.map(async (task) => {
try {
const response = await SessionUser.objects.self_serve_tasks.attachments.list(task.id);
return { ...task, attachments: response.data || [] };
const taskAttachments = response.data || [];
// Fetch download links for images
const attachmentsWithLinks = await Promise.all(taskAttachments.map(async (att) => {
if (isImage(att)) {
try {
const dlResponse = await SessionUser.objects.self_serve_tasks.attachments.download(task.id, att.id);
att.download_link = dlResponse.data?.download_link || dlResponse.download_link;
} catch (err) {
console.error(`Error fetching download link for attachment ${att.id}:`, err);
}
}
return att;
}));
return { ...task, attachments: attachmentsWithLinks };
} catch (e) {
console.error(`Error fetching attachments for task ${task.id}:`, e);
return { ...task, attachments: [] };
@@ -168,6 +183,13 @@ const isTaskActive = (taskId, visited = new Set()) => {
return evaluateCondition(task.condition_id, visited);
};
const isImage = (attachment) => {
const filename = attachment.content?.other || '';
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
const extension = filename.split('.').pop().toLowerCase();
return imageExtensions.includes(extension);
};
const downloadAttachment = async (taskId, attachmentId) => {
try {
const response = await SessionUser.objects.self_serve_tasks.attachments.download(taskId, attachmentId);
@@ -340,11 +362,16 @@ const getConditionById = (id) => {
<strong>{{ t.task }}</strong>
<p v-if="t.description" class="is-size-7">{{ t.description }}</p>
<div v-if="t.attachments && t.attachments.length > 0" class="mt-2">
<div v-for="attachment in t.attachments" :key="attachment.id" class="is-flex is-align-items-center mb-1">
<span class="icon is-small mr-1">
<i class="fas fa-paperclip"></i>
</span>
<a @click="downloadAttachment(t.id, attachment.id)" class="is-size-7">{{ attachment.content?.other || 'Vedhæftet fil' }}</a>
<div v-for="attachment in t.attachments" :key="attachment.id" class="mb-2">
<template v-if="isImage(attachment) && attachment.download_link">
<img :src="attachment.download_link" :alt="attachment.content?.other" style="max-width: 100%; height: auto; border-radius: 4px; display: block;" class="mb-1 is-clickable" @click="downloadAttachment(t.id, attachment.id)">
</template>
<div v-else class="is-flex is-align-items-center">
<span class="icon is-small mr-1">
<i class="fas fa-paperclip"></i>
</span>
<a @click="downloadAttachment(t.id, attachment.id)" class="is-size-7">{{ attachment.content?.other || 'Vedhæftet fil' }}</a>
</div>
</div>
</div>
</div>
@@ -571,7 +571,22 @@ const fetchSelfServeData = async () => {
tasks.value = await Promise.all(filteredTasks.map(async (task) => {
try {
const response = await SessionUser.objects.self_serve_tasks.attachments.list(task.id);
return { ...task, attachments: response.data || [] };
const taskAttachments = response.data || [];
// Fetch download links for images
const attachmentsWithLinks = await Promise.all(taskAttachments.map(async (att) => {
if (isImage(att)) {
try {
const dlResponse = await SessionUser.objects.self_serve_tasks.attachments.download(task.id, att.id);
att.download_link = dlResponse.data?.download_link || dlResponse.download_link;
} catch (err) {
console.error(`Error fetching download link for attachment ${att.id}:`, err);
}
}
return att;
}));
return { ...task, attachments: attachmentsWithLinks };
} catch (e) {
console.error(`Error fetching attachments for task ${task.id}:`, e);
return { ...task, attachments: [] };
@@ -697,6 +712,13 @@ const isTaskActive = (taskId, visited = new Set()) => {
return evaluateCondition(task.condition_id, visited);
};
const isImage = (attachment) => {
const filename = attachment.content?.other || '';
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
const extension = filename.split('.').pop().toLowerCase();
return imageExtensions.includes(extension);
};
const downloadAttachment = async (taskId, attachmentId) => {
try {
const response = await SessionUser.objects.self_serve_tasks.attachments.download(taskId, attachmentId);
@@ -1145,9 +1167,14 @@ watch(() => currentStep.value, () => {
<p><strong>{{ task.task }}</strong></p>
<p class="is-size-7" v-if="task.description">{{ task.description }}</p>
<div v-if="task.attachments && task.attachments.length > 0" class="mt-2">
<div v-for="attachment in task.attachments" :key="attachment.id" class="is-flex is-align-items-center mb-1">
<b-icon pack="fas" icon="paperclip" size="is-small" class="mr-1"></b-icon>
<a @click="downloadAttachment(task.id, attachment.id)" class="is-size-7">{{ attachment.content?.other || 'Vedhæftet fil' }}</a>
<div v-for="attachment in task.attachments" :key="attachment.id" class="mb-2">
<template v-if="isImage(attachment) && attachment.download_link">
<img :src="attachment.download_link" :alt="attachment.content?.other" style="max-width: 100%; height: auto; border-radius: 4px; display: block;" class="mb-1 is-clickable" @click="downloadAttachment(task.id, attachment.id)">
</template>
<div v-else class="is-flex is-align-items-center">
<b-icon pack="fas" icon="paperclip" size="is-small" class="mr-1"></b-icon>
<a @click="downloadAttachment(task.id, attachment.id)" class="is-size-7">{{ attachment.content?.other || 'Vedhæftet fil' }}</a>
</div>
</div>
</div>
</div>