fix(timeline): rehydrate event-form pickers after a validation failure

Decision 6 / REQ-010 promised the pickers survive a fail(400) "including
pre-selected persons/documents", but EventForm only seeded them from
event/initialPersons — never from the fail payload — and the payload carried
only bare ids, which can't rebuild a chip (chips need displayName/title). On
the use:enhance path the in-memory selection survived; on a no-JS full reload
the chips were silently dropped.

Now the save action re-fetches the selected persons/documents by id
(lookupSelections, non-ok swallowed like the prefill path) and returns full
chip data; EventForm seeds the pickers from form.persons/documents ahead of
the seeded event. Extract preservedFormFields() to DRY the four fail payloads;
validateEventForm now returns the error pair and the route owns the fail().

Component test pins the rehydration; the server spec now asserts the fail
payload carries labelled chips, not just ids.

Addresses PR #832 review (Developer + Requirements Engineer concern, REQ-010).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 00:28:28 +02:00
parent cd5649b96e
commit c13baa4785
6 changed files with 133 additions and 41 deletions

View File

@@ -30,6 +30,10 @@ interface FormResult {
type?: string;
personIds?: string[];
documentIds?: string[];
// Rehydrated chip data (id + label) so the pickers re-render after a fail(400)
// even on a no-JS full reload — bare ids alone can't rebuild a chip (REQ-010).
persons?: PersonOption[];
documents?: DocumentOption[];
}
let {
@@ -46,8 +50,10 @@ let {
form?: FormResult | null;
} = $props();
// Initial-state snapshot from incoming props (event > preserved fail payload).
// The form owns these after mount; re-mount with a different `event` to reset.
// Initial-state snapshot from incoming props, preferring a preserved fail payload
// over the seeded `event`. This component is intentionally single-shot: props are
// snapshotted into $state once, so a parent re-render with a different `event`
// won't update the form — the two dedicated routes always remount, which is fine.
let title = $state(form?.title ?? event?.title ?? '');
let description = $state(form?.description ?? event?.description ?? '');
let type = $state<string>(form?.type ?? event?.type ?? 'PERSONAL');
@@ -55,19 +61,23 @@ let dateIso = $state(event?.eventDate ?? '');
let precision = $state<DatePrecision>((event?.precision as DatePrecision) ?? 'DAY');
let endDateIso = $state(event?.eventDateEnd ?? '');
// On a fail(400) the server returns rehydrated chip data (form.persons/documents)
// so the pickers survive the round-trip — even without JS — ahead of the seeded
// `event` or the prefill initials (REQ-010 / Decision 6).
let selectedPersons = $state<PersonOption[]>(
event?.persons ? event.persons.map(toPersonOption) : initialPersons
form?.persons ?? (event?.persons ? event.persons.map(toPersonOption) : initialPersons)
);
let selectedDocuments = $state<DocumentOption[]>(
event?.documents
? event.documents.map((d) => ({
// Graceful degradation: DocumentRef has no precision fields. formatDocumentOption
// defaults a missing precision to DAY, so the chip shows the full documentDate.
id: d.id,
title: d.title,
documentDate: d.documentDate
}))
: initialDocuments
form?.documents ??
(event?.documents
? event.documents.map((d) => ({
// Graceful degradation: DocumentRef has no precision fields. formatDocumentOption
// defaults a missing precision to DAY, so the chip shows the full documentDate.
id: d.id,
title: d.title,
documentDate: d.documentDate
}))
: initialDocuments)
);
const isEdit = $derived(event !== undefined);