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:
Marcel
2026-06-14 09:11:20 +02:00
parent 9f2ae7bd2e
commit 23f6bc284d
8 changed files with 69 additions and 9 deletions

View File

@@ -36,6 +36,7 @@ let {
dateLabel = m.form_label_date(),
dateRequired = true,
dateError = '',
endDateError = '',
onchange = undefined,
dateTestId = undefined,
precisionTestId = undefined,
@@ -53,6 +54,8 @@ let {
dateRequired?: boolean;
/** Server-side date error (e.g. blank required field) wired to the field's aria-invalid. */
dateError?: string;
/** Server-side end-date error (e.g. RANGE without an end date) wired to the end field. */
endDateError?: string;
/** Called on any user edit (date, precision, end-date) — lets a parent track dirtiness. */
onchange?: () => void;
dateTestId?: string;
@@ -99,6 +102,9 @@ const dateFieldInvalid = $derived(dateInvalid || dateError.length > 0);
const endBeforeStart = $derived(
showEndDate && endDateIso !== '' && dateIso !== '' && endDateIso < dateIso
);
// Either the inline end-before-start cue or a server-provided end-date error
// (e.g. a RANGE event missing its end date) marks the end field invalid.
const endDateFieldInvalid = $derived(endBeforeStart || endDateError.length > 0);
function handleDateInput(e: Event) {
const result = handleGermanDateInput(e);
@@ -190,10 +196,10 @@ $effect(() => {
oninput={handleEndDateInput}
placeholder={m.form_placeholder_date()}
maxlength="10"
aria-invalid={endBeforeStart ? 'true' : undefined}
aria-describedby={endBeforeStart ? `${dateInputName}-end-error` : undefined}
aria-invalid={endDateFieldInvalid ? 'true' : undefined}
aria-describedby={endDateFieldInvalid ? `${dateInputName}-end-error` : undefined}
class="block min-h-[48px] w-full rounded border border-line px-2 py-3 text-sm shadow-sm
{endBeforeStart
{endDateFieldInvalid
? 'border-red-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500'
: 'focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}"
/>
@@ -202,6 +208,10 @@ $effect(() => {
<p id="{dateInputName}-end-error" class="mt-1 text-xs text-danger">
<span aria-hidden="true"></span>{m.error_invalid_date_range()}
</p>
{:else if endDateError}
<p id="{dateInputName}-end-error" class="mt-1 text-xs text-danger">
<span aria-hidden="true"></span>{endDateError}
</p>
{/if}
</div>
{/if}