feat(documents): add inbox document preview modal
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fileId: string
|
fileId: string
|
||||||
path?: string | null
|
path?: string | null
|
||||||
|
interactive?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
@@ -18,39 +19,47 @@ const revokeObjectUrl = () => {
|
|||||||
objectUrl.value = null
|
objectUrl.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFile = async () => {
|
watch(() => props.fileId, async (fileId, _previous, onCleanup) => {
|
||||||
revokeObjectUrl()
|
revokeObjectUrl()
|
||||||
loadFailed.value = false
|
loadFailed.value = false
|
||||||
|
let active = true
|
||||||
|
const controller = new AbortController()
|
||||||
|
onCleanup(() => {
|
||||||
|
active = false
|
||||||
|
controller.abort()
|
||||||
|
revokeObjectUrl()
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await $api<Blob>(`/api/files/content/${encodeURIComponent(props.fileId)}`, {
|
const blob = await $api<Blob>(`/api/files/content/${encodeURIComponent(fileId)}`, {
|
||||||
responseType: "blob"
|
responseType: "blob",
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
if (!active) return
|
||||||
objectUrl.value = URL.createObjectURL(blob)
|
objectUrl.value = URL.createObjectURL(blob)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!active) return
|
||||||
loadFailed.value = true
|
loadFailed.value = true
|
||||||
console.error("Dateivorschau konnte nicht geladen werden:", error)
|
console.error("Dateivorschau konnte nicht geladen werden:", error)
|
||||||
}
|
}
|
||||||
}
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(() => props.fileId, loadFile, { immediate: true })
|
|
||||||
onBeforeUnmount(revokeObjectUrl)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="authenticated-file-preview">
|
<div class="authenticated-file-preview" :class="{ 'is-interactive': interactive }">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="objectUrl && isPdf"
|
v-if="objectUrl && isPdf && !loadFailed"
|
||||||
:src="`${objectUrl}#toolbar=0&navpanes=0&scrollbar=0`"
|
:src="interactive ? objectUrl : `${objectUrl}#toolbar=0&navpanes=0&scrollbar=0`"
|
||||||
title="PDF-Vorschau"
|
title="PDF-Vorschau"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<img v-else-if="objectUrl" :src="objectUrl" alt="Dateivorschau" />
|
<img v-else-if="objectUrl && !loadFailed" :src="objectUrl" alt="Dateivorschau" @error="loadFailed = true" />
|
||||||
<div v-else-if="loadFailed" class="preview-placeholder">
|
<div v-else-if="loadFailed" class="preview-placeholder" role="status">
|
||||||
<UIcon name="i-heroicons-document" class="h-10 w-10" />
|
<UIcon name="i-heroicons-document" class="h-10 w-10" />
|
||||||
<span>Keine Vorschau verfügbar</span>
|
<span>Keine Vorschau verfügbar</span>
|
||||||
|
<span v-if="interactive">Du kannst das Dokument herunterladen oder in einem neuen Tab öffnen.</span>
|
||||||
</div>
|
</div>
|
||||||
<USkeleton v-else class="h-full min-h-32 w-full" />
|
<USkeleton v-else class="h-full min-h-32 w-full" aria-label="Dokumentvorschau wird geladen" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -67,6 +76,10 @@ onBeforeUnmount(revokeObjectUrl)
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.authenticated-file-preview.is-interactive iframe {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.authenticated-file-preview img {
|
.authenticated-file-preview img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
@@ -80,5 +93,8 @@ onBeforeUnmount(revokeObjectUrl)
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
color: rgb(107 114 128);
|
color: rgb(107 114 128);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const password = ref("")
|
|||||||
const confirmReset = ref(false)
|
const confirmReset = ref(false)
|
||||||
const scannerPath = ref("/scanner-inbox/")
|
const scannerPath = ref("/scanner-inbox/")
|
||||||
const origin = ref("")
|
const origin = ref("")
|
||||||
|
const previewOpen = ref(false)
|
||||||
|
const previewDocument = ref<InboxDocument | null>(null)
|
||||||
const filingOpen = ref(false)
|
const filingOpen = ref(false)
|
||||||
const filingDocument = ref<InboxDocument | null>(null)
|
const filingDocument = ref<InboxDocument | null>(null)
|
||||||
const filingLoading = ref(false)
|
const filingLoading = ref(false)
|
||||||
@@ -101,6 +103,18 @@ const load = async (silent = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (entry: InboxDocument) => {
|
||||||
|
previewDocument.value = entry
|
||||||
|
previewOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePreviewDocument = () => {
|
||||||
|
const entry = previewDocument.value
|
||||||
|
if (!entry) return
|
||||||
|
previewOpen.value = false
|
||||||
|
openFiling(entry)
|
||||||
|
}
|
||||||
|
|
||||||
const openFiling = async (entry: InboxDocument) => {
|
const openFiling = async (entry: InboxDocument) => {
|
||||||
filingOpen.value = true
|
filingOpen.value = true
|
||||||
filingDocument.value = entry
|
filingDocument.value = entry
|
||||||
@@ -231,6 +245,9 @@ watch(search, () => {
|
|||||||
watch(setupOpen, open => {
|
watch(setupOpen, open => {
|
||||||
if (!open) { setupVersion++; password.value = ""; confirmReset.value = false }
|
if (!open) { setupVersion++; password.value = ""; confirmReset.value = false }
|
||||||
})
|
})
|
||||||
|
watch(previewOpen, open => {
|
||||||
|
if (!open) previewDocument.value = null
|
||||||
|
})
|
||||||
watch(filingFolder, () => {
|
watch(filingFolder, () => {
|
||||||
const defaultType = selectedFolder.value?.standardFiletype
|
const defaultType = selectedFolder.value?.standardFiletype
|
||||||
if (defaultType) filingType.value = defaultType
|
if (defaultType) filingType.value = defaultType
|
||||||
@@ -250,6 +267,8 @@ watch(() => auth.activeTenant, () => {
|
|||||||
requestVersion++
|
requestVersion++
|
||||||
setupVersion++
|
setupVersion++
|
||||||
filingVersion++
|
filingVersion++
|
||||||
|
previewOpen.value = false
|
||||||
|
previewDocument.value = null
|
||||||
filingOpen.value = false
|
filingOpen.value = false
|
||||||
clearTimeout(searchTimer)
|
clearTimeout(searchTimer)
|
||||||
clearTimeout(pollTimer)
|
clearTimeout(pollTimer)
|
||||||
@@ -305,10 +324,10 @@ onBeforeUnmount(() => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-default">
|
<tbody class="divide-y divide-default">
|
||||||
<tr v-for="entry in documents" :key="entry.id" class="hover:bg-elevated/30">
|
<tr v-for="entry in documents" :key="entry.id" class="hover:bg-elevated/30">
|
||||||
<td class="max-w-sm px-4 py-4"><button class="flex items-center gap-3 text-left font-medium text-highlighted hover:text-primary" @click="fileAction(entry, false)"><UIcon name="i-heroicons-document-text" class="h-5 w-5 shrink-0 text-muted" /><span class="break-all">{{ entry.name }}</span></button></td>
|
<td class="max-w-sm px-4 py-4"><button class="flex items-center gap-3 text-left font-medium text-highlighted hover:text-primary" :aria-label="`${entry.name} als Vorschau anzeigen`" @click="openPreview(entry)"><UIcon name="i-heroicons-document-text" class="h-5 w-5 shrink-0 text-muted" /><span class="break-all">{{ entry.name }}</span></button></td>
|
||||||
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ dayjs(entry.receivedAt).format('DD.MM.YYYY HH:mm') }}</td>
|
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ dayjs(entry.receivedAt).format('DD.MM.YYYY HH:mm') }}</td>
|
||||||
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ formatSize(entry.size) }}</td>
|
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ formatSize(entry.size) }}</td>
|
||||||
<td class="px-4 py-4"><div class="flex justify-end gap-1"><UButton icon="i-heroicons-folder-arrow-down" variant="soft" :aria-label="`${entry.name} ablegen`" @click="openFiling(entry)">Ablegen</UButton><UButton icon="i-heroicons-arrow-top-right-on-square" color="neutral" variant="ghost" :aria-label="`${entry.name} öffnen`" @click="fileAction(entry, false)" /><UButton icon="i-heroicons-arrow-down-tray" color="neutral" variant="ghost" :aria-label="`${entry.name} herunterladen`" @click="fileAction(entry, true)" /></div></td>
|
<td class="px-4 py-4"><div class="flex justify-end gap-1"><UButton icon="i-heroicons-folder-arrow-down" variant="soft" :aria-label="`${entry.name} ablegen`" @click="openFiling(entry)">Ablegen</UButton><UButton icon="i-heroicons-eye" color="neutral" variant="ghost" :aria-label="`${entry.name} als Vorschau anzeigen`" @click="openPreview(entry)">Vorschau</UButton><UButton icon="i-heroicons-arrow-down-tray" color="neutral" variant="ghost" :aria-label="`${entry.name} herunterladen`" @click="fileAction(entry, true)" /></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -323,12 +342,31 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
<UModal v-model:open="previewOpen" :title="previewDocument?.name || 'Dokumentvorschau'" description="Vorschau des eingegangenen Dokuments" :ui="{ content: 'w-[calc(100vw-2rem)] max-w-6xl h-[90dvh]', body: 'min-h-0 flex-1 overflow-hidden p-0 sm:p-0', title: 'break-all pr-8', footer: 'flex-wrap justify-end' }">
|
||||||
|
<template #body>
|
||||||
|
<AuthenticatedFilePreview
|
||||||
|
v-if="previewOpen && previewDocument"
|
||||||
|
:key="previewDocument.id"
|
||||||
|
:file-id="previewDocument.id"
|
||||||
|
:path="previewDocument.name"
|
||||||
|
interactive
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<template v-if="previewDocument">
|
||||||
|
<UButton icon="i-heroicons-arrow-top-right-on-square" color="neutral" variant="ghost" @click="fileAction(previewDocument, false)">In neuem Tab öffnen</UButton>
|
||||||
|
<UButton icon="i-heroicons-arrow-down-tray" color="neutral" variant="outline" @click="fileAction(previewDocument, true)">Herunterladen</UButton>
|
||||||
|
<UButton v-if="!filingOpen" icon="i-heroicons-folder-arrow-down" @click="filePreviewDocument">In Dateien ablegen</UButton>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
<UModal v-model:open="filingOpen" title="In Dateien ablegen" description="Wähle den Zielordner und klassifiziere das Dokument." :dismissible="!filingSaving" :close="!filingSaving">
|
<UModal v-model:open="filingOpen" title="In Dateien ablegen" description="Wähle den Zielordner und klassifiziere das Dokument." :dismissible="!filingSaving" :close="!filingSaving">
|
||||||
<template #body>
|
<template #body>
|
||||||
<form class="space-y-5" @submit.prevent="fileDocument">
|
<form class="space-y-5" @submit.prevent="fileDocument">
|
||||||
<div v-if="filingDocument" class="flex items-center justify-between gap-3 rounded-lg bg-elevated/50 p-3">
|
<div v-if="filingDocument" class="flex items-center justify-between gap-3 rounded-lg bg-elevated/50 p-3">
|
||||||
<span class="break-all text-sm font-medium">{{ filingDocument.name }}</span>
|
<span class="break-all text-sm font-medium">{{ filingDocument.name }}</span>
|
||||||
<UButton icon="i-heroicons-arrow-top-right-on-square" color="neutral" variant="ghost" aria-label="Dokument öffnen" @click="fileAction(filingDocument, false)" />
|
<UButton icon="i-heroicons-eye" color="neutral" variant="ghost" aria-label="Dokumentvorschau öffnen" @click="openPreview(filingDocument)" />
|
||||||
</div>
|
</div>
|
||||||
<UAlert v-if="filingError" :title="filingError" color="error" variant="subtle" />
|
<UAlert v-if="filingError" :title="filingError" color="error" variant="subtle" />
|
||||||
<USkeleton v-if="filingLoading" class="h-32 w-full" />
|
<USkeleton v-if="filingLoading" class="h-32 w-full" />
|
||||||
|
|||||||
Reference in New Issue
Block a user