From 58ba130cd90964fe94799312a3f3e9ef277edb67 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 16:08:18 +0200 Subject: [PATCH] feat: Passwort-Manager Mehrfachauswahl + Bulk-Loeschen Checkbox pro Eintrag, "Alle auswaehlen" oben und roter Loesch-Button mit Anzahl. Sicherheitsabfrage vor dem Loeschen. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/views/PasswordsView.vue | 59 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/PasswordsView.vue b/frontend/src/views/PasswordsView.vue index 3ebacdd..321c45c 100644 --- a/frontend/src/views/PasswordsView.vue +++ b/frontend/src/views/PasswordsView.vue @@ -29,9 +29,22 @@ +
+ + +
+
+ class="entry-item" :class="{ selected: selectedIds.includes(entry.id) }" + @click="openEntry(entry)"> +
@@ -166,6 +179,7 @@ import InputText from 'primevue/inputtext' import Password from 'primevue/password' import Textarea from 'primevue/textarea' import Select from 'primevue/select' +import Checkbox from 'primevue/checkbox' const toast = useToast() const auth = useAuthStore() @@ -200,6 +214,45 @@ const importAccept = computed(() => { const showTotpDialog = ref(false) const totpCode = ref('') +const selectedIds = ref([]) +const allSelected = computed({ + get: () => filteredEntries.value.length > 0 && filteredEntries.value.every(e => selectedIds.value.includes(e.id)), + set: () => {}, +}) + +function toggleSelectAll() { + const visibleIds = filteredEntries.value.map(e => e.id) + const allSel = visibleIds.every(id => selectedIds.value.includes(id)) + if (allSel) { + selectedIds.value = selectedIds.value.filter(id => !visibleIds.includes(id)) + } else { + const set = new Set([...selectedIds.value, ...visibleIds]) + selectedIds.value = [...set] + } +} + +function toggleSelect(id) { + const i = selectedIds.value.indexOf(id) + if (i >= 0) selectedIds.value.splice(i, 1) + else selectedIds.value.push(id) +} + +async function deleteSelected() { + const n = selectedIds.value.length + if (!n) return + if (!window.confirm(`${n} Eintrag/Eintraege wirklich loeschen?`)) return + let ok = 0 + for (const id of [...selectedIds.value]) { + try { + await apiClient.delete(`/passwords/entries/${id}`) + ok++ + } catch { /* skip */ } + } + selectedIds.value = [] + toast.add({ severity: 'success', summary: `${ok} Eintrag/Eintraege geloescht`, life: 3000 }) + await loadEntries() +} + const folderOptions = computed(() => [{ id: null, name: '(Kein Ordner)' }, ...folders.value]) const filteredEntries = computed(() => { if (!searchQuery.value) return entries.value @@ -491,6 +544,10 @@ onMounted(async () => { .shared-label { color: var(--p-text-muted-color); font-size: 0.75rem; } .entries-main { flex: 1; } .search-bar { margin-bottom: 1rem; } +.selection-bar { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; background: var(--p-surface-50); border-radius: 6px; } +.select-all-label { font-size: 0.875rem; cursor: pointer; flex: 1; } +.selected-count { color: var(--p-text-muted-color); margin-left: 0.5rem; } +.entry-item.selected { background: var(--p-primary-50); } .entries-list { display: flex; flex-direction: column; gap: 2px; } .entry-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; background: var(--p-surface-0); border-radius: 6px; cursor: pointer; } .entry-item:hover { background: var(--p-surface-100); }