feat(bulk-edit): add /documents/bulk-edit route
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 <noreply@anthropic.com>
This commit is contained in:
10
frontend/src/routes/documents/bulk-edit/+page.server.ts
Normal file
10
frontend/src/routes/documents/bulk-edit/+page.server.ts
Normal file
@@ -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 };
|
||||
}
|
||||
53
frontend/src/routes/documents/bulk-edit/+page.svelte
Normal file
53
frontend/src/routes/documents/bulk-edit/+page.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { bulkSelectionStore } from '$lib/stores/bulkSelection.svelte';
|
||||
import BulkDocumentEditLayout, {
|
||||
type BulkEditEntry
|
||||
} from '$lib/components/document/BulkDocumentEditLayout.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let entries = $state<BulkEditEntry[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
const ids = Array.from(bulkSelectionStore.ids);
|
||||
if (ids.length === 0) {
|
||||
await goto('/documents');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/documents/batch-metadata', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
});
|
||||
if (!res.ok) {
|
||||
error = m.error_internal_error();
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
const summaries = (await res.json()) as BulkEditEntry[];
|
||||
entries = summaries;
|
||||
} catch {
|
||||
error = m.error_internal_error();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.bulk_edit_title()} – Familienarchiv</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex h-full items-center justify-center p-12 text-sm text-ink-2">…</div>
|
||||
{:else if error}
|
||||
<div class="m-6 rounded-sm border border-red-300 bg-red-50 p-4 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
{:else if entries.length > 0}
|
||||
<BulkDocumentEditLayout mode="edit" initialEditEntries={entries} />
|
||||
{/if}
|
||||
46
frontend/src/routes/documents/bulk-edit/page.server.spec.ts
Normal file
46
frontend/src/routes/documents/bulk-edit/page.server.spec.ts
Normal file
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user