From 25446c9a5c528d2a121042eb21382e24e7864f81 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 25 Apr 2026 14:54:59 +0200 Subject: [PATCH] feat(bulk-edit): add bulkSelection store backed by SvelteSet Module-singleton live accumulator: selection persists across pagination and route changes within /documents and /enrich. Cleared on successful bulk save or via Alles aufheben. Refs #225 Co-Authored-By: Claude Sonnet 4.6 --- .../lib/stores/bulkSelection.svelte.spec.ts | 53 +++++++++++++++++++ .../src/lib/stores/bulkSelection.svelte.ts | 36 +++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 frontend/src/lib/stores/bulkSelection.svelte.spec.ts create mode 100644 frontend/src/lib/stores/bulkSelection.svelte.ts diff --git a/frontend/src/lib/stores/bulkSelection.svelte.spec.ts b/frontend/src/lib/stores/bulkSelection.svelte.spec.ts new file mode 100644 index 00000000..cde4814a --- /dev/null +++ b/frontend/src/lib/stores/bulkSelection.svelte.spec.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { bulkSelectionStore } from './bulkSelection.svelte'; + +describe('bulkSelectionStore', () => { + afterEach(() => bulkSelectionStore.clear()); + + it('starts empty', () => { + expect(bulkSelectionStore.size).toBe(0); + }); + + it('toggle adds an id when absent', () => { + bulkSelectionStore.toggle('a'); + expect(bulkSelectionStore.has('a')).toBe(true); + expect(bulkSelectionStore.size).toBe(1); + }); + + it('toggle removes an id when present', () => { + bulkSelectionStore.add('a'); + bulkSelectionStore.toggle('a'); + expect(bulkSelectionStore.has('a')).toBe(false); + }); + + it('add and remove update size', () => { + bulkSelectionStore.add('a'); + bulkSelectionStore.add('b'); + expect(bulkSelectionStore.size).toBe(2); + bulkSelectionStore.remove('a'); + expect(bulkSelectionStore.size).toBe(1); + expect(bulkSelectionStore.has('b')).toBe(true); + }); + + it('add is idempotent', () => { + bulkSelectionStore.add('a'); + bulkSelectionStore.add('a'); + expect(bulkSelectionStore.size).toBe(1); + }); + + it('setAll replaces the selection', () => { + bulkSelectionStore.add('a'); + bulkSelectionStore.add('b'); + bulkSelectionStore.setAll(['c', 'd', 'e']); + expect(bulkSelectionStore.size).toBe(3); + expect(bulkSelectionStore.has('a')).toBe(false); + expect(bulkSelectionStore.has('c')).toBe(true); + }); + + it('clear empties the selection', () => { + bulkSelectionStore.add('a'); + bulkSelectionStore.add('b'); + bulkSelectionStore.clear(); + expect(bulkSelectionStore.size).toBe(0); + }); +}); diff --git a/frontend/src/lib/stores/bulkSelection.svelte.ts b/frontend/src/lib/stores/bulkSelection.svelte.ts new file mode 100644 index 00000000..1d1f7863 --- /dev/null +++ b/frontend/src/lib/stores/bulkSelection.svelte.ts @@ -0,0 +1,36 @@ +import { SvelteSet } from 'svelte/reactivity'; + +// Live accumulator. Selection persists across pagination and route changes +// within /documents and /enrich. Cleared on successful bulk save or via +// "Alles aufheben". The store is module-singleton — there is only ever one +// bulk-edit selection per browser session. +const selectedIds = new SvelteSet(); + +export const bulkSelectionStore = { + get ids(): SvelteSet { + return selectedIds; + }, + get size(): number { + return selectedIds.size; + }, + has(id: string): boolean { + return selectedIds.has(id); + }, + toggle(id: string): void { + if (selectedIds.has(id)) selectedIds.delete(id); + else selectedIds.add(id); + }, + add(id: string): void { + selectedIds.add(id); + }, + remove(id: string): void { + selectedIds.delete(id); + }, + setAll(ids: Iterable): void { + selectedIds.clear(); + for (const id of ids) selectedIds.add(id); + }, + clear(): void { + selectedIds.clear(); + } +};