feat(bulk-edit): add BulkSelectionBar and Alle-X-editieren fast path

- BulkSelectionBar component: sticky bottom bar shown only when canWrite
  and selection is non-empty. Buttons meet WCAG 44px touch targets and
  iOS safe-area inset is honoured.
- Bar mounted on /documents and /enrich.
- Alle X editieren button on /documents replaces the selection with
  every UUID matching the active filter (via /api/documents/ids) and
  jumps to /documents/bulk-edit.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-25 15:07:26 +02:00
parent 27e3d290e7
commit d4f32ed5d4
4 changed files with 154 additions and 0 deletions

View File

@@ -6,6 +6,8 @@ import { SvelteURLSearchParams } from 'svelte/reactivity';
import SearchFilterBar from '../SearchFilterBar.svelte';
import DocumentList from '../DocumentList.svelte';
import Pagination from '$lib/components/Pagination.svelte';
import BulkSelectionBar from '$lib/components/document/BulkSelectionBar.svelte';
import { bulkSelectionStore } from '$lib/stores/bulkSelection.svelte';
import * as m from '$lib/paraglide/messages.js';
let { data } = $props();
@@ -138,6 +140,45 @@ $effect(() => {
}
});
let editingAll = $state(false);
/**
* Fast path: replace the current selection with every document matching the
* active filter (across all pages) and jump to the bulk-edit screen. The
* /api/documents/ids endpoint is uncapped — chunking happens at PATCH time
* inside the bulk-edit page's save handler.
*/
async function editAllMatching() {
if (editingAll) return;
editingAll = true;
try {
const params = buildSearchParams({
q: data.q || '',
from: data.from || '',
to: data.to || '',
senderId: data.senderId || '',
receiverId: data.receiverId || '',
tags: data.tags || [],
sort: '',
dir: '',
tagQ: data.tagQ || '',
tagOp: (data.tagOp as 'AND' | 'OR') || 'AND'
});
params.delete('sort');
params.delete('dir');
const res = await fetch(`/api/documents/ids?${params.toString()}`);
if (!res.ok) {
editingAll = false;
return;
}
const ids: string[] = await res.json();
bulkSelectionStore.setAll(ids);
await goto('/documents/bulk-edit');
} finally {
editingAll = false;
}
}
// Keep local filter state in sync with server data after navigation completes.
// Guard q: skip overwrite while the user is actively typing.
$effect(() => {
@@ -181,6 +222,20 @@ $effect(() => {
onblur={() => (qFocused = false)}
/>
{#if data.canWrite && data.totalElements > 0}
<div class="mb-2 flex justify-end">
<button
type="button"
onclick={editAllMatching}
disabled={editingAll}
class="inline-flex items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink disabled:opacity-50"
data-testid="bulk-edit-all-x"
>
{m.bulk_edit_all_x({ count: data.totalElements })}
</button>
</div>
{/if}
<DocumentList
items={data.items}
total={data.totalElements}
@@ -192,3 +247,5 @@ $effect(() => {
<Pagination page={data.pageNumber} totalPages={data.totalPages} makeHref={buildPageHref} />
</main>
<BulkSelectionBar canWrite={data.canWrite} />