Files
EpicNext-Cms/src/features/housekeeping/domains/economy/pages/economy-command-form.tsx
T
Simo 43470ebf82
CI / check (pull_request) Failing after 10s
CI / deploy (pull_request) Skipped
CI / release (pull_request) Skipped
feat(housekeeping): unify complete rebuild in gated preview
2026-08-31 20:37:51 +02:00

200 lines
5.1 KiB
TypeScript

"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<Record<string, unknown>>;
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<unknown> | null = null;
export async function submitEconomyCommandForm(
configuration: EconomyCommandSubmission,
_previous: HousekeepingResult<unknown> | null,
formData: FormData,
): Promise<HousekeepingResult<unknown>> {
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 (
<form
action={submit}
data-housekeeping-command={commandId}
className="space-y-3 rounded border border-[var(--admin-border)] p-3"
>
{fields.map((field) => {
const fieldId = `economy-${commandId}-${field.name}`;
return (
<label key={field.name} htmlFor={fieldId} className="block text-sm">
{field.type === "checkbox" && field.allowUnchanged ? (
<>
{field.label}
<select
id={fieldId}
name={field.name}
defaultValue=""
className="mt-1 block w-full"
>
<option value="">No change</option>
<option value="true">Enabled</option>
<option value="false">Disabled</option>
</select>
</>
) : field.type === "checkbox" ? (
<>
<input
id={fieldId}
name={field.name}
type="checkbox"
defaultChecked={field.defaultValue === true}
/>{" "}
{field.label}
</>
) : field.type === "textarea" ? (
<>
{field.label}
<textarea
id={fieldId}
name={field.name}
required={field.required}
maxLength={field.maxLength}
className="mt-1 block w-full"
/>
</>
) : (
<>
{field.label}
<input
id={fieldId}
name={field.name}
type={
field.type === "number"
? "number"
: field.type === "file"
? "file"
: "text"
}
required={field.required}
min={field.min}
max={field.max}
maxLength={field.maxLength}
className="mt-1 block w-full"
/>
</>
)}
</label>
);
})}
{requiresReason ? (
<label
htmlFor={`economy-${commandId}-reason`}
className="block text-sm"
>
Reason
<textarea
id={`economy-${commandId}-reason`}
name="reason"
required
maxLength={1000}
/>
</label>
) : null}
<button type="submit" disabled={pending}>
{pending ? "Working..." : buttonLabel}
</button>
{result ? (
<p role="status">
{result.ok ? "Completed" : "Failed"} ({result.correlationId})
</p>
) : null}
</form>
);
}