feat(timeline): add /zeitstrahl/events/[id]/edit curator edit + delete route
Load gates on hasWriteAll (null-user guard first, 403 error page) and seeds the
form from GET /api/timeline/events/{id}, failing closed with 404 on ANY non-ok
response so derived person-events (no UUID) and unknown ids never render a blank
create form. The save action PUTs with the optimistic-lock version (threaded via
a hidden input EventForm now emits), mapping 409 to the generic conflict message
without redirecting. The delete action DELETEs behind getConfirmService(),
returns fail(status) on a non-ok response (no redirect), and otherwise redirects
to the UUID-validated nav target. 8/8 server specs green; EventForm 6/6 green.
Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
107
frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts
Normal file
107
frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import { hasWriteAll } from '$lib/shared/server/permissions';
|
||||
import {
|
||||
parseEventForm,
|
||||
validateEventForm,
|
||||
toEventRequest,
|
||||
resolveNavTarget
|
||||
} from '$lib/timeline/eventFormServer';
|
||||
|
||||
export async function load({
|
||||
locals,
|
||||
params,
|
||||
url,
|
||||
fetch
|
||||
}: {
|
||||
locals: App.Locals;
|
||||
params: { id: string };
|
||||
url: URL;
|
||||
fetch: typeof globalThis.fetch;
|
||||
}) {
|
||||
// Null-user guard first — avoids a TypeError on locals.user.groups for an
|
||||
// unauthenticated request that reaches the route.
|
||||
if (!locals.user) throw error(403, 'Forbidden');
|
||||
// WRITE_ALL check mirrors Permission.WRITE_ALL — server-side gate; frontend
|
||||
// canWrite flag is for hiding entry-point buttons only.
|
||||
if (!hasWriteAll(locals)) throw error(403, 'Forbidden');
|
||||
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.GET('/api/timeline/events/{id}', {
|
||||
params: { path: { id: params.id } }
|
||||
});
|
||||
|
||||
// Fail closed: derived person-events (Geburt/Tod/Heirat) are not persisted and
|
||||
// have no UUID, so the API 404s for them. Any non-ok response → 404; never
|
||||
// render a blank editable form that silently POSTs a new event.
|
||||
if (!result.response.ok) throw error(404, 'Not found');
|
||||
|
||||
return { event: result.data!, originPersonId: url.searchParams.get('personId') ?? '' };
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
save: async ({
|
||||
request,
|
||||
params,
|
||||
fetch
|
||||
}: {
|
||||
request: Request;
|
||||
params: { id: string };
|
||||
fetch: typeof globalThis.fetch;
|
||||
}) => {
|
||||
const formData = await request.formData();
|
||||
const parsed = parseEventForm(formData);
|
||||
|
||||
const invalid = validateEventForm(parsed);
|
||||
if (invalid) return invalid;
|
||||
|
||||
const versionRaw = formData.get('version')?.toString();
|
||||
const version = versionRaw ? Number(versionRaw) : undefined;
|
||||
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.PUT('/api/timeline/events/{id}', {
|
||||
params: { path: { id: params.id } },
|
||||
body: toEventRequest(parsed, version)
|
||||
});
|
||||
|
||||
if (!result.response.ok) {
|
||||
return fail(result.response.status, {
|
||||
error: getErrorMessage(extractErrorCode(result.error)),
|
||||
title: parsed.title,
|
||||
description: parsed.description,
|
||||
type: parsed.type,
|
||||
personIds: parsed.personIds,
|
||||
documentIds: parsed.documentIds
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect(303, resolveNavTarget(parsed.originPersonId));
|
||||
},
|
||||
|
||||
delete: async ({
|
||||
request,
|
||||
params,
|
||||
fetch
|
||||
}: {
|
||||
request: Request;
|
||||
params: { id: string };
|
||||
fetch: typeof globalThis.fetch;
|
||||
}) => {
|
||||
const formData = await request.formData();
|
||||
const originPersonId = formData.get('originPersonId')?.toString() ?? '';
|
||||
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.DELETE('/api/timeline/events/{id}', {
|
||||
params: { path: { id: params.id } }
|
||||
});
|
||||
|
||||
if (!result.response.ok) {
|
||||
return fail(result.response.status, {
|
||||
error: getErrorMessage(extractErrorCode(result.error))
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect(303, resolveNavTarget(originPersonId));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user