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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-12 16:08:18 +02:00
parent 230c83f124
commit 58ba130cd9
1 changed files with 58 additions and 1 deletions

View File

@ -29,9 +29,22 @@
<InputText v-model="searchQuery" placeholder="Passwoerter suchen..." fluid />
</div>
<div v-if="filteredEntries.length" class="selection-bar">
<Checkbox v-model="allSelected" :binary="true" @change="toggleSelectAll" inputId="select-all" />
<label for="select-all" class="select-all-label">
Alle auswaehlen
<span v-if="selectedIds.length" class="selected-count">({{ selectedIds.length }} ausgewaehlt)</span>
</label>
<Button v-if="selectedIds.length" icon="pi pi-trash" :label="`${selectedIds.length} loeschen`"
severity="danger" size="small" @click="deleteSelected" />
</div>
<div class="entries-list">
<div v-for="entry in filteredEntries" :key="entry.id"
class="entry-item" @click="openEntry(entry)">
class="entry-item" :class="{ selected: selectedIds.includes(entry.id) }"
@click="openEntry(entry)">
<Checkbox :modelValue="selectedIds.includes(entry.id)" :binary="true"
@click.stop @update:modelValue="toggleSelect(entry.id)" />
<div class="entry-icon">
<i class="pi pi-key"></i>
</div>
@ -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); }