From 6d3489d0358108f66375364483cf2c613500044c Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 25 Apr 2026 15:18:07 +0200 Subject: [PATCH] feat(bulk-edit): add /documents/bulk-edit route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server load redirects READ_ALL-only users (or unauthenticated) to /documents. Page load: onMount reads bulkSelectionStore — redirects to /documents when the store is empty, otherwise POSTs the IDs to /api/documents/batch-metadata and hands the resulting summaries to BulkDocumentEditLayout in mode="edit". Refs #225 Co-Authored-By: Claude Sonnet 4.6 --- .../documents/bulk-edit/+page.server.ts | 10 ++++ .../routes/documents/bulk-edit/+page.svelte | 53 +++++++++++++++++++ .../documents/bulk-edit/page.server.spec.ts | 46 ++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 frontend/src/routes/documents/bulk-edit/+page.server.ts create mode 100644 frontend/src/routes/documents/bulk-edit/+page.svelte create mode 100644 frontend/src/routes/documents/bulk-edit/page.server.spec.ts diff --git a/frontend/src/routes/documents/bulk-edit/+page.server.ts b/frontend/src/routes/documents/bulk-edit/+page.server.ts new file mode 100644 index 00000000..33c845a0 --- /dev/null +++ b/frontend/src/routes/documents/bulk-edit/+page.server.ts @@ -0,0 +1,10 @@ +import { redirect } from '@sveltejs/kit'; + +export async function load({ locals }: { locals: App.Locals }) { + const canWrite = + locals.user?.groups?.some((g: { permissions: string[] }) => + g.permissions.includes('WRITE_ALL') + ) ?? false; + if (!canWrite) throw redirect(303, '/documents'); + return { canWrite }; +} diff --git a/frontend/src/routes/documents/bulk-edit/+page.svelte b/frontend/src/routes/documents/bulk-edit/+page.svelte new file mode 100644 index 00000000..b4018b8e --- /dev/null +++ b/frontend/src/routes/documents/bulk-edit/+page.svelte @@ -0,0 +1,53 @@ + + + + {m.bulk_edit_title()} – Familienarchiv + + +{#if loading} +
+{:else if error} +
+ {error} +
+{:else if entries.length > 0} + +{/if} diff --git a/frontend/src/routes/documents/bulk-edit/page.server.spec.ts b/frontend/src/routes/documents/bulk-edit/page.server.spec.ts new file mode 100644 index 00000000..9f52a1a0 --- /dev/null +++ b/frontend/src/routes/documents/bulk-edit/page.server.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { load } from './+page.server'; + +describe('/documents/bulk-edit +page.server.ts', () => { + it('redirects to /documents when user lacks WRITE_ALL', async () => { + const locals = { user: { groups: [{ permissions: ['READ_ALL'] }] } }; + try { + // @ts-expect-error — partial event shape sufficient for this guard + await load({ locals }); + throw new Error('expected redirect to be thrown'); + } catch (e) { + const err = e as { status?: number; location?: string }; + expect(err.status).toBe(303); + expect(err.location).toBe('/documents'); + } + }); + + it('redirects when user has no groups', async () => { + const locals = { user: { groups: [] } }; + try { + // @ts-expect-error — partial event shape sufficient for this guard + await load({ locals }); + throw new Error('expected redirect'); + } catch (e) { + expect((e as { status?: number }).status).toBe(303); + } + }); + + it('redirects when no user is logged in', async () => { + const locals = {}; + try { + // @ts-expect-error — partial event shape sufficient for this guard + await load({ locals }); + throw new Error('expected redirect'); + } catch (e) { + expect((e as { status?: number }).status).toBe(303); + } + }); + + it('returns canWrite=true for a WRITE_ALL user', async () => { + const locals = { user: { groups: [{ permissions: ['WRITE_ALL', 'READ_ALL'] }] } }; + // @ts-expect-error — partial event shape sufficient for this guard + const result = await load({ locals }); + expect(result).toEqual({ canWrite: true }); + }); +});