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:
parent
230c83f124
commit
58ba130cd9
|
|
@ -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); }
|
||||
|
|
|
|||
Loading…
Reference in New Issue