From 3b2012e7042b638b295f6313c86ae54a40f45be5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 2 Sep 2026 19:38:38 +0200 Subject: [PATCH] feat(housekeeping): retain invalid command input --- .../content/pages/content-command-form.tsx | 307 ++++++++++++------ .../content/pages/content-pages.test.tsx | 125 ++++++- 2 files changed, 326 insertions(+), 106 deletions(-) diff --git a/src/features/housekeeping/domains/content/pages/content-command-form.tsx b/src/features/housekeeping/domains/content/pages/content-command-form.tsx index 8871d39a..fe02a7d7 100644 --- a/src/features/housekeeping/domains/content/pages/content-command-form.tsx +++ b/src/features/housekeeping/domains/content/pages/content-command-form.tsx @@ -99,14 +99,43 @@ function parseField(field: ContentCommandField, formData: FormData): unknown { ); } -const initialState: HousekeepingResult | null = null; +export interface ContentCommandFormState { + readonly result: HousekeepingResult | null; + readonly values: Readonly>; +} + +const initialState: ContentCommandFormState = { result: null, values: {} }; + +function readSubmittedValues( + fields: readonly ContentCommandField[], + requiresReason: boolean, + formData: FormData, +): Readonly> { + const values: Record = {}; + for (const field of fields) { + if (field.type === "file") continue; + const value = formData.get(field.name); + if (typeof value === "string") values[field.name] = value; + } + if (requiresReason) { + const reason = formData.get("reason"); + if (typeof reason === "string") values.reason = reason; + } + return values; +} export async function submitContentCommandForm( configuration: ContentCommandSubmission, - _previous: HousekeepingResult | null, + _previous: ContentCommandFormState | null, formData: FormData, -): Promise> { - const submittedFields = (configuration.fields ?? []).flatMap((field) => { +): Promise { + const fields = configuration.fields ?? []; + const submittedValues = readSubmittedValues( + fields, + configuration.requiresReason ?? false, + formData, + ); + const submittedFields = fields.flatMap((field) => { const value = parseField(field, formData); return value === OMIT_FIELD ? [] : [[field.name, value] as const]; }); @@ -121,11 +150,31 @@ export async function submitContentCommandForm( const { executeHousekeepingCommand } = await import( "@/actions/housekeeping-command" ); - return executeHousekeepingCommand({ + const result = await executeHousekeepingCommand({ commandId: configuration.commandId, input, ...(configuration.requiresReason ? { reason } : {}), }); + return { result, values: result.ok ? {} : submittedValues }; +} + +export function ContentCommandFieldError({ + result, + fieldName, + errorId, +}: { + readonly result: HousekeepingResult; + readonly fieldName: string; + readonly errorId: string; +}) { + if (result.ok) return null; + const messages = result.error.fieldErrors?.[fieldName]; + if (!messages || messages.length === 0) return null; + return ( + + ); } export function ContentCommandForm({ @@ -136,7 +185,7 @@ export function ContentCommandForm({ requiresReason = false, }: ContentCommandFormProps) { const formId = useId(); - const [result, submit, pending] = useActionState( + const [state, submit, pending] = useActionState( submitContentCommandForm.bind(null, { commandId, input, @@ -145,121 +194,168 @@ export function ContentCommandForm({ }), initialState, ); + const result = state.result; + const retainedValues = result && !result.ok ? state.values : {}; + const remountKey = `${formId}-${result?.correlationId ?? "initial"}`; + const reasonErrorId = `${formId}-reason-error`; + const hasReasonError = + !result || result.ok + ? false + : Boolean(result.error.fieldErrors?.reason?.length); + return (
- {fields.map((field) => ( -