feat: Unterordner-Navigation in Ordner-Freigaben

Geteilte Ordner koennen jetzt komplett durchnavigiert werden:

- Klick auf einen Unterordner oeffnet dessen Inhalt
- Breadcrumb-Navigation: Root > Unterordner > Unter-Unterordner
  mit Klick zurueck auf jede Ebene
- Ordner-Zeilen haben Hover-Effekt und Cursor-Pointer

Backend:
- GET /share/<token>/files?parent_id=X - Unterordner auflisten
- Sicherheitspruefung: parent_id muss innerhalb des geteilten
  Ordners liegen (_is_inside_shared_folder traversiert den Baum)
- Breadcrumb wird serverseitig berechnet
- Download + Delete erlauben jetzt Dateien in jeder Tiefe
  (nicht nur direkte Kinder)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 20:17:35 +02:00
parent 7a26788ad2
commit 6515b3a256
2 changed files with 90 additions and 26 deletions
+40 -7
View File
@@ -38,8 +38,16 @@
<!-- Folder: file listing (read + write) -->
<div v-if="fileInfo.is_folder && fileInfo.permission !== 'upload_only'" class="folder-content">
<div class="folder-breadcrumb">
<a class="crumb" @click="navigateToRoot">{{ fileInfo.name }}</a>
<template v-for="item in folderBreadcrumb" :key="item.id">
<i class="pi pi-angle-right crumb-sep"></i>
<a class="crumb" @click="navigateToFolder(item.id)">{{ item.name }}</a>
</template>
</div>
<div class="folder-toolbar">
<span class="folder-count">{{ folderFiles.length }} Dateien</span>
<span class="folder-count">{{ folderFiles.length }} Eintraege</span>
<Button v-if="fileInfo.upload_allowed" icon="pi pi-upload" label="Hochladen" size="small" @click="$refs.uploadInput.click()" />
</div>
@@ -48,7 +56,8 @@
</div>
<div v-else class="file-list">
<div v-for="f in folderFiles" :key="f.id" class="file-item">
<div v-for="f in folderFiles" :key="f.id" class="file-item"
:class="{ clickable: f.is_folder }" @click="f.is_folder && navigateToFolder(f.id)">
<div class="file-info">
<i :class="f.is_folder ? 'pi pi-folder' : 'pi pi-file'"></i>
<div class="file-details">
@@ -58,9 +67,9 @@
</div>
<div class="file-actions">
<Button v-if="!f.is_folder" icon="pi pi-download" text size="small"
@click="downloadFolderFile(f)" />
@click.stop="downloadFolderFile(f)" />
<Button v-if="fileInfo.permission === 'write'" icon="pi pi-trash" text size="small"
severity="danger" @click="deleteFolderFile(f)" />
severity="danger" @click.stop="deleteFolderFile(f)" />
</div>
</div>
<div v-if="!folderFiles.length" class="empty-folder">
@@ -122,6 +131,8 @@ const authError = ref('')
const verifying = ref(false)
const folderFiles = ref([])
const folderBreadcrumb = ref([])
const currentParentId = ref(null)
const loadingFiles = ref(false)
const isDragging = ref(false)
@@ -179,18 +190,31 @@ async function verifyPassword() {
}
}
async function loadFolderFiles() {
async function loadFolderFiles(parentId = null) {
loadingFiles.value = true
try {
const res = await axios.get(`/api/share/${token}/files`, { headers: getAuthHeaders() })
folderFiles.value = res.data
const params = {}
if (parentId) params.parent_id = parentId
const res = await axios.get(`/api/share/${token}/files`, { params, headers: getAuthHeaders() })
folderFiles.value = res.data.files
folderBreadcrumb.value = res.data.breadcrumb || []
currentParentId.value = res.data.current_parent_id
} catch {
folderFiles.value = []
folderBreadcrumb.value = []
} finally {
loadingFiles.value = false
}
}
function navigateToFolder(folderId) {
loadFolderFiles(folderId)
}
function navigateToRoot() {
loadFolderFiles(null)
}
function downloadFile() {
let url = `/api/share/${token}/download`
if (fileInfo.value?.has_password && password.value) {
@@ -287,6 +311,13 @@ onMounted(loadInfo)
/* Folder content */
.folder-content { margin-bottom: 1.5rem; }
.folder-breadcrumb {
display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.crumb { cursor: pointer; color: var(--p-primary-color); font-weight: 500; }
.crumb:hover { text-decoration: underline; }
.crumb-sep { font-size: 0.7rem; color: var(--p-surface-400); }
.folder-toolbar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--p-surface-200);
@@ -297,6 +328,8 @@ onMounted(loadInfo)
display: flex; align-items: center; justify-content: space-between;
padding: 0.5rem 0; border-bottom: 1px solid var(--p-surface-100);
}
.file-item.clickable { cursor: pointer; }
.file-item.clickable:hover { background: var(--p-surface-50); }
.file-info { display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0; }
.file-info i { font-size: 1.1rem; color: var(--p-surface-500); flex-shrink: 0; }
.file-details { display: flex; flex-direction: column; min-width: 0; }