"use client"; import { useActionState } from "react"; import type { HousekeepingResult } from "../../../foundation/contracts"; export interface EconomyCommandField { readonly name: string; readonly label: string; readonly type: | "identifier" | "text" | "textarea" | "number" | "checkbox" | "file"; readonly required?: boolean; readonly min?: number; readonly max?: number; readonly maxLength?: number; readonly defaultValue?: string | number | boolean; readonly allowUnchanged?: boolean; } export interface EconomyCommandSubmission { readonly commandId: string; readonly input: Readonly>; readonly fields?: readonly EconomyCommandField[]; readonly requiresReason?: boolean; } interface EconomyCommandFormProps extends EconomyCommandSubmission { readonly buttonLabel: string; } const OMIT_FIELD = Symbol("omit optional Economy command field"); function parseField(field: EconomyCommandField, formData: FormData): unknown { const rawValue = formData.get(field.name); if (field.type === "checkbox") { if (field.allowUnchanged) { if (rawValue === null || rawValue === "") return OMIT_FIELD; if (rawValue === "true") return true; if (rawValue === "false") return false; return OMIT_FIELD; } return rawValue === "on"; } if (rawValue === null && !field.required) return OMIT_FIELD; if (field.type === "file") return rawValue instanceof File ? rawValue : null; const raw = String(rawValue ?? "") .normalize("NFC") .trim(); if (!raw && !field.required) return OMIT_FIELD; if (field.type === "number") { const value = Number(raw); if (!Number.isSafeInteger(value)) return 0; return Math.min(field.max ?? value, Math.max(field.min ?? value, value)); } return raw.slice( 0, field.maxLength ?? (field.type === "textarea" ? 20_000 : 500), ); } const initialState: HousekeepingResult | null = null; export async function submitEconomyCommandForm( configuration: EconomyCommandSubmission, _previous: HousekeepingResult | null, formData: FormData, ): Promise> { const submitted = (configuration.fields ?? []).flatMap((field) => { const value = parseField(field, formData); return value === OMIT_FIELD ? [] : [[field.name, value] as const]; }); const reason = String(formData.get("reason") ?? "") .normalize("NFC") .trim() .slice(0, 1000); const { executeHousekeepingCommand } = await import( "@/actions/housekeeping-command" ); return executeHousekeepingCommand({ commandId: configuration.commandId, input: { ...configuration.input, ...Object.fromEntries(submitted) }, ...(configuration.requiresReason ? { reason } : {}), }); } export function EconomyCommandForm({ commandId, buttonLabel, input, fields = [], requiresReason = false, }: EconomyCommandFormProps) { const [result, submit, pending] = useActionState( submitEconomyCommandForm.bind(null, { commandId, input, fields, requiresReason, }), initialState, ); return (
{fields.map((field) => { const fieldId = `economy-${commandId}-${field.name}`; return (