feat: Papierkorb + Bestaetigungsdialoge bei allen Loeschaktionen

Papierkorb:
- Dateien/Ordner werden beim Loeschen in den Papierkorb verschoben
  (Soft-Delete) statt sofort geloescht
- Papierkorb-Seite in der Sidebar mit Tabelle aller geloeschten Elemente
- Pro Element: Wiederherstellen (am Originalort) oder endgueltig loeschen
- "Papierkorb leeren" Button loescht alles unwiderruflich
- Backend: is_trashed, trashed_at, original_parent_id Felder im File-Model
- Getrashte Dateien erscheinen nicht in der normalen Dateiliste

Bestaetigungsdialoge (vorher fehlend):
- Kontakte: "Moechtest du XY wirklich loeschen?"
- Kalender Events: Bestaetigung vor dem Loeschen
- Kalender: Bestaetigung vor dem Loeschen (mit Hinweis auf Events)
- E-Mail Nachrichten: Bestaetigung mit Betreff-Vorschau
- Share-Link Dateien: Bestaetigung beim Loeschen aus geteiltem Ordner
- Admin SFTP-Backup-Ziele: Bestaetigung
- Admin Email-Konten: Bestaetigung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-11 20:50:19 +02:00
parent 1ee80e650d
commit 82f3091f2e
10 changed files with 423 additions and 26 deletions

View File

@ -62,8 +62,8 @@ def list_files():
user = request.current_user
parent_id = request.args.get('parent_id', None, type=int)
# Own files in this folder
query = File.query.filter_by(owner_id=user.id, parent_id=parent_id)
# Own files in this folder (exclude trashed)
query = File.query.filter_by(owner_id=user.id, parent_id=parent_id, is_trashed=False)
files = query.order_by(File.is_folder.desc(), File.name).all()
# Shared files at root level
@ -328,7 +328,7 @@ def update_file(file_id):
return jsonify(f.to_dict()), 200
# --- Delete ---
# --- Delete (soft-delete -> trash) ---
@api_bp.route('/files/<int:file_id>', methods=['DELETE'])
@token_required
@ -336,21 +336,37 @@ def delete_file(file_id):
user = request.current_user
f, err = _get_file_or_403(file_id, user, 'admin')
if err:
# Owner can always delete
f = db.session.get(File, file_id)
if not f or f.owner_id != user.id:
return jsonify({'error': 'Zugriff verweigert'}), 403
_delete_recursive(f, user.id)
# Soft-delete: move to trash
_trash_recursive(f)
db.session.commit()
return jsonify({'message': 'Geloescht'}), 200
return jsonify({'message': 'In Papierkorb verschoben'}), 200
def _delete_recursive(file_obj, user_id):
def _trash_recursive(file_obj):
"""Move file/folder to trash (soft-delete)."""
now = datetime.now(timezone.utc)
if not file_obj.is_trashed:
file_obj.original_parent_id = file_obj.parent_id
file_obj.parent_id = None
file_obj.is_trashed = True
file_obj.trashed_at = now
if file_obj.is_folder:
children = File.query.filter_by(parent_id=file_obj.id, is_trashed=False).all()
for child in children:
child.is_trashed = True
child.trashed_at = now
def _delete_permanent(file_obj):
"""Permanently delete a file/folder and its disk data."""
if file_obj.is_folder:
children = File.query.filter_by(parent_id=file_obj.id).all()
for child in children:
_delete_recursive(child, user_id)
_delete_permanent(child)
else:
if file_obj.storage_path:
filepath = Path(current_app.config['UPLOAD_PATH']) / str(file_obj.owner_id) / file_obj.storage_path
@ -359,6 +375,100 @@ def _delete_recursive(file_obj, user_id):
db.session.delete(file_obj)
# --- Trash / Papierkorb ---
@api_bp.route('/files/trash', methods=['GET'])
@token_required
def list_trash():
"""List all trashed files for the current user."""
user = request.current_user
trashed = File.query.filter_by(owner_id=user.id, is_trashed=True)\
.filter(File.original_parent_id.isnot(None) | (File.original_parent_id.is_(None)))\
.order_by(File.trashed_at.desc()).all()
# Only show top-level trashed items (not children of trashed folders)
top_level = []
trashed_folder_ids = {f.id for f in trashed if f.is_folder}
for f in trashed:
# Show if parent is not also trashed
parent_trashed = False
if f.original_parent_id:
parent = db.session.get(File, f.original_parent_id)
if parent and parent.is_trashed:
parent_trashed = True
if not parent_trashed:
top_level.append(f.to_dict())
return jsonify(top_level), 200
@api_bp.route('/files/trash/<int:file_id>/restore', methods=['POST'])
@token_required
def restore_from_trash(file_id):
"""Restore a file/folder from trash."""
user = request.current_user
f = db.session.get(File, file_id)
if not f or f.owner_id != user.id or not f.is_trashed:
return jsonify({'error': 'Nicht gefunden'}), 404
# Restore to original parent (or root if parent no longer exists)
original_parent = db.session.get(File, f.original_parent_id) if f.original_parent_id else None
if original_parent and not original_parent.is_trashed:
f.parent_id = f.original_parent_id
else:
f.parent_id = None
f.is_trashed = False
f.trashed_at = None
f.original_parent_id = None
# Also restore children
if f.is_folder:
_restore_children(f)
db.session.commit()
return jsonify({'message': 'Wiederhergestellt'}), 200
def _restore_children(folder):
children = File.query.filter_by(is_trashed=True).all()
for child in children:
# Check if this child was inside the restored folder
if child.original_parent_id == folder.id or child.parent_id == folder.id:
child.is_trashed = False
child.trashed_at = None
child.parent_id = folder.id
child.original_parent_id = None
if child.is_folder:
_restore_children(child)
@api_bp.route('/files/trash/<int:file_id>', methods=['DELETE'])
@token_required
def delete_permanently(file_id):
"""Permanently delete a trashed file."""
user = request.current_user
f = db.session.get(File, file_id)
if not f or f.owner_id != user.id or not f.is_trashed:
return jsonify({'error': 'Nicht gefunden'}), 404
_delete_permanent(f)
db.session.commit()
return jsonify({'message': 'Endgueltig geloescht'}), 200
@api_bp.route('/files/trash/empty', methods=['POST'])
@token_required
def empty_trash():
"""Permanently delete all trashed files."""
user = request.current_user
trashed = File.query.filter_by(owner_id=user.id, is_trashed=True).all()
for f in trashed:
_delete_permanent(f)
db.session.commit()
return jsonify({'message': 'Papierkorb geleert'}), 200
# --- Permissions ---
@api_bp.route('/files/<int:file_id>/permissions', methods=['GET'])

View File

@ -15,6 +15,9 @@ class File(db.Model):
size = db.Column(db.BigInteger, default=0)
storage_path = db.Column(db.String(500), nullable=True) # UUID-based path on disk
checksum = db.Column(db.String(64), nullable=True) # SHA-256 for sync
is_trashed = db.Column(db.Boolean, default=False, nullable=False, index=True)
trashed_at = db.Column(db.DateTime, nullable=True)
original_parent_id = db.Column(db.Integer, nullable=True) # to restore to original location
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
@ -28,7 +31,7 @@ class File(db.Model):
cascade='all, delete-orphan')
def to_dict(self):
return {
d = {
'id': self.id,
'owner_id': self.owner_id,
'parent_id': self.parent_id,
@ -39,6 +42,10 @@ class File(db.Model):
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
if self.is_trashed:
d['is_trashed'] = True
d['trashed_at'] = self.trashed_at.isoformat() if self.trashed_at else None
return d
class FilePermission(db.Model):

View File

@ -28,6 +28,11 @@ const routes = [
name: 'Files',
component: () => import('../views/FilesView.vue'),
},
{
path: 'trash',
name: 'Trash',
component: () => import('../views/TrashView.vue'),
},
{
path: 'calendar',
name: 'Calendar',

View File

@ -180,7 +180,7 @@
<Button icon="pi pi-list" label="Versionen" size="small" outlined @click="openVersions(tgt)" />
<Button icon="pi pi-check-circle" label="Testen" size="small" text @click="testTarget(tgt)" />
<Button icon="pi pi-pencil" text size="small" @click="openEditTarget(tgt)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click="deleteSftpTarget(tgt)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click="confirmDeleteSftp(tgt)" />
</div>
</div>
</div>
@ -374,7 +374,7 @@
</div>
<div class="acc-actions">
<Button icon="pi pi-pencil" text size="small" @click="openEditEmailAccount(acc)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click="deleteEmailAccount(acc)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click="confirmDeleteEmail(acc)" />
</div>
</div>
</div>
@ -451,6 +451,24 @@
<Button label="Loeschen" severity="danger" @click="doDeleteUser" />
</template>
</Dialog>
<!-- Confirm delete SFTP target -->
<Dialog v-model:visible="showDeleteSftpConfirm" header="Backup-Ziel loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du das Backup-Ziel <strong>{{ deleteSftpTarget_ref?.name }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteSftpConfirm = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteSftpTarget" />
</template>
</Dialog>
<!-- Confirm delete email account -->
<Dialog v-model:visible="showDeleteEmailConfirm" header="E-Mail-Konto loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du das E-Mail-Konto <strong>{{ deleteEmailTarget?.display_name }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteEmailConfirm = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteEmailTarget" />
</template>
</Dialog>
</div>
</template>
@ -560,6 +578,10 @@ const filteredUsers = computed(() => {
const showDeleteConfirm = ref(false)
const deleteTarget = ref(null)
const showDeleteSftpConfirm = ref(false)
const deleteSftpTarget_ref = ref(null)
const showDeleteEmailConfirm = ref(false)
const deleteEmailTarget = ref(null)
// Email accounts for edited user
const userEmailAccounts = ref([])
@ -674,9 +696,16 @@ async function saveTarget() {
}
}
async function deleteSftpTarget(tgt) {
function confirmDeleteSftp(tgt) {
deleteSftpTarget_ref.value = tgt
showDeleteSftpConfirm.value = true
}
async function doDeleteSftpTarget() {
if (!deleteSftpTarget_ref.value) return
try {
await apiClient.delete(`/admin/backup/targets/${tgt.id}`)
await apiClient.delete(`/admin/backup/targets/${deleteSftpTarget_ref.value.id}`)
showDeleteSftpConfirm.value = false
await loadTargets()
toast.add({ severity: 'success', summary: 'Backup-Ziel geloescht', life: 3000 })
} catch (err) {
@ -1054,10 +1083,17 @@ async function saveEmailAccount() {
}
}
async function deleteEmailAccount(acc) {
function confirmDeleteEmail(acc) {
deleteEmailTarget.value = acc
showDeleteEmailConfirm.value = true
}
async function doDeleteEmailTarget() {
if (!deleteEmailTarget.value) return
try {
await apiClient.delete(`/admin/email-accounts/${acc.id}`)
userEmailAccounts.value = userEmailAccounts.value.filter(a => a.id !== acc.id)
await apiClient.delete(`/admin/email-accounts/${deleteEmailTarget.value.id}`)
userEmailAccounts.value = userEmailAccounts.value.filter(a => a.id !== deleteEmailTarget.value.id)
showDeleteEmailConfirm.value = false
toast.add({ severity: 'success', summary: 'Konto geloescht', life: 3000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })

View File

@ -36,6 +36,11 @@
<i class="pi pi-key"></i>
<span>Passwoerter</span>
</router-link>
<router-link to="/trash" class="nav-item" active-class="active">
<i class="pi pi-trash"></i>
<span>Papierkorb</span>
</router-link>
</nav>
<div class="sidebar-footer">

View File

@ -85,7 +85,7 @@
<label><input type="checkbox" v-model="eventForm.all_day" /> Ganztaegig</label>
</div>
<template #footer>
<Button v-if="editingEvent" label="Loeschen" severity="danger" text @click="deleteEvent" />
<Button v-if="editingEvent" label="Loeschen" severity="danger" text @click="confirmDeleteEvent = true" />
<Button label="Abbrechen" text @click="showEventDialog = false" />
<Button :label="editingEvent ? 'Speichern' : 'Erstellen'" @click="saveEvent" />
</template>
@ -114,9 +114,27 @@
</div>
<Button v-if="selectedCal.permission === 'owner'" label="Kalender loeschen"
severity="danger" text size="small" @click="deleteCalendar" />
severity="danger" text size="small" @click="confirmDeleteCal = true" />
</div>
</Dialog>
<!-- Confirm delete event -->
<Dialog v-model:visible="confirmDeleteEvent" header="Event loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du <strong>{{ editingEvent?.summary }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="confirmDeleteEvent = false" />
<Button label="Loeschen" severity="danger" @click="deleteEvent; confirmDeleteEvent = false" />
</template>
</Dialog>
<!-- Confirm delete calendar -->
<Dialog v-model:visible="confirmDeleteCal" header="Kalender loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du den Kalender <strong>{{ selectedCal?.name }}</strong> mit allen Events loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="confirmDeleteCal = false" />
<Button label="Loeschen" severity="danger" @click="deleteCalendar(); confirmDeleteCal = false" />
</template>
</Dialog>
</div>
</template>
@ -147,6 +165,8 @@ const selectedCal = ref(null)
const shareUsername = ref('')
const sharePermission = ref('read')
const icalUrl = ref('')
const confirmDeleteEvent = ref(false)
const confirmDeleteCal = ref(false)
const permOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Lesen+Schreiben', value: 'readwrite' }]
const weekDays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']

View File

@ -31,7 +31,7 @@
<Column field="phone" header="Telefon" />
<Column header="" style="width: 80px">
<template #body="{ data }">
<Button icon="pi pi-trash" text size="small" severity="danger" @click.stop="deleteContact(data)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click.stop="confirmDeleteContact(data)" />
</template>
</Column>
</DataTable>
@ -77,6 +77,15 @@
<Button :label="editingContact ? 'Speichern' : 'Erstellen'" @click="saveContact" />
</template>
</Dialog>
<!-- Delete Confirm -->
<Dialog v-model:visible="showDeleteConfirm" header="Kontakt loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du <strong>{{ deleteTarget?.display_name }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteConfirm = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteContact" />
</template>
</Dialog>
</div>
</template>
@ -178,8 +187,18 @@ async function saveContact() {
}
}
async function deleteContact(contact) {
await apiClient.delete(`/contacts/${contact.id}`)
const showDeleteConfirm = ref(false)
const deleteTarget = ref(null)
function confirmDeleteContact(contact) {
deleteTarget.value = contact
showDeleteConfirm.value = true
}
async function doDeleteContact() {
if (!deleteTarget.value) return
await apiClient.delete(`/contacts/${deleteTarget.value.id}`)
showDeleteConfirm.value = false
await loadContacts()
await loadBooks()
}

View File

@ -54,7 +54,7 @@
</div>
<div class="msg-actions">
<Button icon="pi pi-reply" label="Antworten" size="small" outlined @click="replyTo" />
<Button icon="pi pi-trash" size="small" severity="danger" text @click="deleteMessage" />
<Button icon="pi pi-trash" size="small" severity="danger" text @click="confirmDeleteMsg = true" />
</div>
</div>
<div v-if="messageDetail.html_body" class="msg-body" v-html="messageDetail.html_body"></div>
@ -100,6 +100,16 @@
<Button label="Senden" icon="pi pi-send" @click="sendEmail" :loading="sending" />
</template>
</Dialog>
<!-- Delete message confirm -->
<Dialog v-model:visible="confirmDeleteMsg" header="Nachricht loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du diese Nachricht wirklich loeschen?</p>
<p v-if="messageDetail" class="msg-subject-preview"><strong>{{ messageDetail.subject }}</strong></p>
<template #footer>
<Button label="Abbrechen" text @click="confirmDeleteMsg = false" />
<Button label="Loeschen" severity="danger" @click="deleteMessage(); confirmDeleteMsg = false" />
</template>
</Dialog>
</div>
</template>
@ -125,6 +135,7 @@ const currentFolderName = ref('INBOX')
const currentAccount = ref(null)
const loadingMessages = ref(false)
const confirmDeleteMsg = ref(false)
const showCompose = ref(false)
const sending = ref(false)
const composeForm = ref({ account_id: null, to: '', cc: '', subject: '', body_text: '' })

View File

@ -73,7 +73,7 @@
<Button v-if="!f.is_folder" icon="pi pi-download" text size="small"
@click.stop="downloadFolderFile(f)" />
<Button v-if="fileInfo.permission === 'write'" icon="pi pi-trash" text size="small"
severity="danger" @click.stop="deleteFolderFile(f)" />
severity="danger" @click.stop="confirmDeleteShareFile(f)" />
</div>
</div>
<div v-if="!folderFiles.length" class="empty-folder">
@ -111,6 +111,15 @@
</div>
</div>
</div>
<!-- Delete confirm for shared folder files -->
<Dialog v-model:visible="showShareDeleteConfirm" header="Datei loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du <strong>{{ shareDeleteTarget?.name }}</strong> wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showShareDeleteConfirm = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteShareFile" />
</template>
</Dialog>
</div>
</template>
@ -122,6 +131,7 @@ import Button from 'primevue/button'
import Password from 'primevue/password'
import Message from 'primevue/message'
import ProgressBar from 'primevue/progressbar'
import Dialog from 'primevue/dialog'
const route = useRoute()
const token = route.params.token
@ -139,6 +149,8 @@ const folderBreadcrumb = ref([])
const currentParentId = ref(null)
const loadingFiles = ref(false)
const showShareDeleteConfirm = ref(false)
const shareDeleteTarget = ref(null)
const isDragging = ref(false)
const uploading = ref(false)
const uploadPercent = ref(0)
@ -243,10 +255,17 @@ function downloadFolderFile(file) {
window.location.href = url
}
async function deleteFolderFile(file) {
function confirmDeleteShareFile(file) {
shareDeleteTarget.value = file
showShareDeleteConfirm.value = true
}
async function doDeleteShareFile() {
if (!shareDeleteTarget.value) return
try {
await axios.delete(`/api/share/${token}/files/${file.id}`, { headers: getAuthHeaders() })
folderFiles.value = folderFiles.value.filter(f => f.id !== file.id)
await axios.delete(`/api/share/${token}/files/${shareDeleteTarget.value.id}`, { headers: getAuthHeaders() })
folderFiles.value = folderFiles.value.filter(f => f.id !== shareDeleteTarget.value.id)
showShareDeleteConfirm.value = false
} catch (err) {
alert(err.response?.data?.error || 'Fehler beim Loeschen')
}

View File

@ -0,0 +1,165 @@
<template>
<div class="view-container">
<div class="view-header">
<h2>Papierkorb</h2>
<Button v-if="trashItems.length" icon="pi pi-trash" label="Papierkorb leeren"
size="small" severity="danger" outlined @click="confirmEmpty = true" />
</div>
<div v-if="loading" class="loading">
<i class="pi pi-spin pi-spinner"></i> Laden...
</div>
<div v-else-if="!trashItems.length" class="empty-state">
<i class="pi pi-trash" style="font-size: 3rem; color: var(--p-surface-300)"></i>
<p>Der Papierkorb ist leer</p>
</div>
<DataTable v-else :value="trashItems" striped-rows>
<Column field="name" header="Name" sortable>
<template #body="{ data }">
<div class="file-name">
<i :class="data.is_folder ? 'pi pi-folder' : 'pi pi-file'"></i>
<span>{{ data.name }}</span>
</div>
</template>
</Column>
<Column field="size" header="Groesse" sortable style="width: 120px">
<template #body="{ data }">
{{ data.is_folder ? '' : formatSize(data.size) }}
</template>
</Column>
<Column field="trashed_at" header="Geloescht am" sortable style="width: 180px">
<template #body="{ data }">
{{ formatDate(data.trashed_at) }}
</template>
</Column>
<Column header="Aktionen" style="width: 160px">
<template #body="{ data }">
<Button icon="pi pi-replay" label="Wiederherstellen" text size="small"
@click.stop="restoreItem(data)" />
<Button icon="pi pi-times" text size="small" severity="danger"
title="Endgueltig loeschen" @click.stop="confirmPermanentDelete(data)" />
</template>
</Column>
</DataTable>
<!-- Confirm permanent delete -->
<Dialog v-model:visible="showDeleteConfirm" header="Endgueltig loeschen" modal :style="{ width: '420px' }">
<p>Moechtest du <strong>{{ deleteTarget?.name }}</strong> wirklich endgueltig loeschen?</p>
<p class="warn-text">Diese Aktion kann nicht rueckgaengig gemacht werden!</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteConfirm = false" />
<Button label="Endgueltig loeschen" severity="danger" @click="deletePermanently" />
</template>
</Dialog>
<!-- Confirm empty trash -->
<Dialog v-model:visible="confirmEmpty" header="Papierkorb leeren" modal :style="{ width: '420px' }">
<p>Moechtest du den gesamten Papierkorb endgueltig leeren?</p>
<p class="warn-text">Alle {{ trashItems.length }} Eintraege werden unwiderruflich geloescht!</p>
<template #footer>
<Button label="Abbrechen" text @click="confirmEmpty = false" />
<Button label="Papierkorb leeren" severity="danger" @click="emptyTrash" />
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import apiClient from '../api/client'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
const toast = useToast()
const trashItems = ref([])
const loading = ref(false)
const showDeleteConfirm = ref(false)
const deleteTarget = ref(null)
const confirmEmpty = ref(false)
function formatSize(bytes) {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let i = 0; let size = bytes
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`
}
function formatDate(iso) {
if (!iso) return ''
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
}
async function loadTrash() {
loading.value = true
try {
const res = await apiClient.get('/files/trash')
trashItems.value = res.data
} catch {
trashItems.value = []
} finally {
loading.value = false
}
}
async function restoreItem(item) {
try {
await apiClient.post(`/files/trash/${item.id}/restore`)
toast.add({ severity: 'success', summary: `"${item.name}" wiederhergestellt`, life: 3000 })
await loadTrash()
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
}
}
function confirmPermanentDelete(item) {
deleteTarget.value = item
showDeleteConfirm.value = true
}
async function deletePermanently() {
if (!deleteTarget.value) return
try {
await apiClient.delete(`/files/trash/${deleteTarget.value.id}`)
toast.add({ severity: 'success', summary: 'Endgueltig geloescht', life: 3000 })
showDeleteConfirm.value = false
await loadTrash()
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
}
}
async function emptyTrash() {
try {
await apiClient.post('/files/trash/empty')
toast.add({ severity: 'success', summary: 'Papierkorb geleert', life: 3000 })
confirmEmpty.value = false
await loadTrash()
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
}
}
onMounted(loadTrash)
</script>
<style scoped>
.view-container { padding: 1.5rem; }
.view-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.view-header h2 { margin: 0; }
.loading { text-align: center; padding: 2rem; color: var(--p-text-muted-color); }
.empty-state {
display: flex; flex-direction: column; align-items: center;
gap: 0.75rem; padding: 4rem 2rem; color: var(--p-text-muted-color);
}
.file-name { display: flex; align-items: center; gap: 0.5rem; }
.warn-text { color: var(--p-red-500); font-size: 0.875rem; }
</style>