fix(timeline): preserve date/precision/end across a fail(400)

preservedFormFields echoed back only title/type/description/pickers, and
EventForm seeded dateIso/precision/endDateIso solely from `event` (undefined
on /new). So a no-JS validation-error reload silently dropped the entire
When-section the curator had entered, while every other field survived.

Echo eventDate/precision/eventDateEnd in the fail payload and seed the date
controls from `form` ahead of `event`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 09:06:21 +02:00
parent 4d5fa7a26f
commit 9f2ae7bd2e
4 changed files with 51 additions and 3 deletions

View File

@@ -28,6 +28,10 @@ interface FormResult {
title?: string;
description?: string;
type?: string;
// When-section values preserved across a fail(400) so a no-JS reload re-seeds them.
eventDate?: string;
precision?: string;
eventDateEnd?: string | null;
personIds?: string[];
documentIds?: string[];
// Rehydrated chip data (id + label) so the pickers re-render after a fail(400)
@@ -57,9 +61,11 @@ let {
let title = $state(form?.title ?? event?.title ?? '');
let description = $state(form?.description ?? event?.description ?? '');
let type = $state<string>(form?.type ?? event?.type ?? 'PERSONAL');
let dateIso = $state(event?.eventDate ?? '');
let precision = $state<DatePrecision>((event?.precision as DatePrecision) ?? 'DAY');
let endDateIso = $state(event?.eventDateEnd ?? '');
let dateIso = $state(form?.eventDate ?? event?.eventDate ?? '');
let precision = $state<DatePrecision>(
(form?.precision as DatePrecision) ?? (event?.precision as DatePrecision) ?? 'DAY'
);
let endDateIso = $state(form?.eventDateEnd ?? 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

View File

@@ -132,6 +132,22 @@ describe('EventForm — delete is gated behind confirmation (REQ-006)', () => {
});
});
describe('EventForm — seeds the When-section from the fail payload (review #2 — no-JS)', () => {
it('re-seeds date, precision and end-date from the form payload on /new', async () => {
renderForm({
form: { eventDate: '1944-03-12', precision: 'RANGE', eventDateEnd: '1944-03-14' }
});
// precision=RANGE seeded → end-date field revealed (proves precision survived).
await expect.element(page.getByLabelText('Enddatum')).toBeVisible();
const dateInput = document.querySelector('#eventDate') as HTMLInputElement;
expect(dateInput.value).toBe('12.03.1944');
const endInput = document.querySelector('#eventDateEnd') as HTMLInputElement;
expect(endInput.value).toBe('14.03.1944');
});
});
describe('EventForm — server date error wired per-field (REQ-011)', () => {
it('marks the date field aria-invalid and shows the message on a server date error', async () => {
renderForm({ form: { dateError: 'Bitte ein Datum eingeben.' } });

View File

@@ -90,6 +90,11 @@ export function preservedFormFields(parsed: ParsedEventForm) {
title: parsed.title,
description: parsed.description,
type: parsed.type,
// The When-section too, so a no-JS full reload re-seeds the date controls
// instead of dropping the curator's date/precision/end-date.
eventDate: parsed.eventDate,
precision: parsed.precision,
eventDateEnd: parsed.eventDateEnd,
personIds: parsed.personIds,
documentIds: parsed.documentIds
};

View File

@@ -187,6 +187,27 @@ describe('zeitstrahl/events/new save action (REQ-004/009/010/015)', () => {
expect((failData(result).documents as { id: string }[])[0].id).toBe('d1');
});
it('preserves date, precision and end-date in the fail payload (review #2 — no-JS reload)', async () => {
// A blank-title fail must echo back the entered When-section so a no-JS full
// reload re-seeds it; otherwise the curator loses the date/precision/end-date.
vi.mocked(createApiClient).mockReturnValue({ POST: vi.fn(), GET: vi.fn() } as never);
const result = await actions.save(
saveEvent({
title: '',
type: 'PERSONAL',
eventDate: '1944-03-12',
precision: 'RANGE',
eventDateEnd: '1944-03-14'
})
);
expect(result).toMatchObject({ status: 400 });
expect(failData(result).eventDate).toBe('1944-03-12');
expect(failData(result).precision).toBe('RANGE');
expect(failData(result).eventDateEnd).toBe('1944-03-14');
});
it('surfaces both title and date errors when both blank (REQ-011)', async () => {
vi.mocked(createApiClient).mockReturnValue({ POST: vi.fn() } as never);
const result = await actions.save(saveEvent({ title: '', type: 'PERSONAL', eventDate: '' }));