fix(timeline): validate RANGE end-date client-side with a field-level error
A RANGE event with a blank end date passed validateEventForm and reached the backend, which 400s with a generic INVALID_DATE_RANGE mapped to "end must not be before start" — wrong for a missing end date, and shown only as a top-of-form alert. Validate it before the API call and surface a dedicated event_editor_end_date_required message on the end-date field via a new DatePrecisionField endDateError prop (defaults '', so the document form is unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ interface FormResult {
|
||||
error?: string;
|
||||
titleError?: string;
|
||||
dateError?: string;
|
||||
endDateError?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
@@ -104,6 +105,9 @@ const titleError = $derived(
|
||||
titleEmpty && (titleTouched || !!form?.titleError) ? m.event_editor_title_required() : ''
|
||||
);
|
||||
const dateError = $derived(dateIso ? '' : (form?.dateError ?? ''));
|
||||
// Only meaningful for RANGE; clears as soon as an end date is entered. The
|
||||
// end-date field is hidden off-RANGE, so a stale value never renders.
|
||||
const endDateError = $derived(endDateIso ? '' : (form?.endDateError ?? ''));
|
||||
|
||||
beforeNavigate(({ cancel }) => {
|
||||
if (dirty && !submitting) {
|
||||
@@ -224,6 +228,7 @@ function markDirty() {
|
||||
precisionInputName="precision"
|
||||
dateLabel={m.form_label_date()}
|
||||
dateError={dateError}
|
||||
endDateError={endDateError}
|
||||
onchange={markDirty}
|
||||
dateTestId="event-date"
|
||||
precisionTestId="event-precision"
|
||||
|
||||
@@ -148,6 +148,21 @@ describe('EventForm — seeds the When-section from the fail payload (review #2
|
||||
});
|
||||
});
|
||||
|
||||
describe('EventForm — RANGE end-date required error wired per-field (review #3)', () => {
|
||||
it('shows the end-date required message on the end-date field, marked invalid', async () => {
|
||||
renderForm({
|
||||
form: {
|
||||
precision: 'RANGE',
|
||||
eventDate: '1925-04-01',
|
||||
endDateError: 'Bitte ein Enddatum eingeben.'
|
||||
}
|
||||
});
|
||||
await expect.element(page.getByText('Bitte ein Enddatum eingeben.')).toBeInTheDocument();
|
||||
const endInput = document.querySelector('#eventDateEnd') as HTMLInputElement;
|
||||
expect(endInput.getAttribute('aria-invalid')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
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.' } });
|
||||
|
||||
@@ -71,17 +71,23 @@ export function parseEventForm(formData: FormData): ParsedEventForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns both failing required-field errors (title + date) simultaneously, or
|
||||
* null when the form is valid. The route owns the `fail(400)` so it can enrich
|
||||
* the payload with the preserved field values and rehydrated picker selections.
|
||||
* Returns the failing required-field errors (title + date + RANGE end-date)
|
||||
* simultaneously, or null when the form is valid. The route owns the `fail(400)`
|
||||
* so it can enrich the payload with the preserved field values and rehydrated
|
||||
* picker selections.
|
||||
*/
|
||||
export function validateEventForm(
|
||||
parsed: ParsedEventForm
|
||||
): { titleError: string; dateError: string } | null {
|
||||
): { titleError: string; dateError: string; endDateError: string } | null {
|
||||
const titleError = parsed.title.length === 0 ? m.event_editor_title_required() : '';
|
||||
const dateError = parsed.eventDate.length === 0 ? m.event_editor_date_required() : '';
|
||||
if (!titleError && !dateError) return null;
|
||||
return { titleError, dateError };
|
||||
// A RANGE event requires an end date. Catch it here so it never reaches the
|
||||
// backend, which rejects with a generic INVALID_DATE_RANGE mapped to the wrong
|
||||
// "end before start" message and no field-level cue.
|
||||
const endDateError =
|
||||
parsed.precision === 'RANGE' && !parsed.eventDateEnd ? m.event_editor_end_date_required() : '';
|
||||
if (!titleError && !dateError && !endDateError) return null;
|
||||
return { titleError, dateError, endDateError };
|
||||
}
|
||||
|
||||
/** The entered field values echoed back in every `fail(...)` so the form re-renders without loss. */
|
||||
|
||||
Reference in New Issue
Block a user