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:
@@ -29,9 +29,22 @@
|
|||||||
<InputText v-model="searchQuery" placeholder="Passwoerter suchen..." fluid />
|
<InputText v-model="searchQuery" placeholder="Passwoerter suchen..." fluid />
|
||||||
</div>
|
</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 class="entries-list">
|
||||||
<div v-for="entry in filteredEntries" :key="entry.id"
|
<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">
|
<div class="entry-icon">
|
||||||
<i class="pi pi-key"></i>
|
<i class="pi pi-key"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,6 +179,7 @@ import InputText from 'primevue/inputtext'
|
|||||||
import Password from 'primevue/password'
|
import Password from 'primevue/password'
|
||||||
import Textarea from 'primevue/textarea'
|
import Textarea from 'primevue/textarea'
|
||||||
import Select from 'primevue/select'
|
import Select from 'primevue/select'
|
||||||
|
import Checkbox from 'primevue/checkbox'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -200,6 +214,45 @@ const importAccept = computed(() => {
|
|||||||
const showTotpDialog = ref(false)
|
const showTotpDialog = ref(false)
|
||||||
const totpCode = ref('')
|
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 folderOptions = computed(() => [{ id: null, name: '(Kein Ordner)' }, ...folders.value])
|
||||||
const filteredEntries = computed(() => {
|
const filteredEntries = computed(() => {
|
||||||
if (!searchQuery.value) return entries.value
|
if (!searchQuery.value) return entries.value
|
||||||
@@ -491,6 +544,10 @@ onMounted(async () => {
|
|||||||
.shared-label { color: var(--p-text-muted-color); font-size: 0.75rem; }
|
.shared-label { color: var(--p-text-muted-color); font-size: 0.75rem; }
|
||||||
.entries-main { flex: 1; }
|
.entries-main { flex: 1; }
|
||||||
.search-bar { margin-bottom: 1rem; }
|
.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; }
|
.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 { 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); }
|
.entry-item:hover { background: var(--p-surface-100); }
|
||||||
|
|||||||
Reference in New Issue
Block a user