fix(housekeeping): retain unchecked command fields

This commit is contained in:
Simo committed 2026-09-02 19:46:19 +02:00
1 parent 3b2012e704
commit 5edaa0254c
2 files changed
+105 -4

No files matched your search

@@ -41,6 +41,7 @@ interface ContentCommandFormProps extends ContentCommandSubmission {
}
const OMIT_FIELD = Symbol("omit optional Content command field");
const UNCHECKED_CHECKBOX_VALUE = "false";
function optionalEmptyValue(field: ContentCommandField): unknown {
return field.emptyValue === null || field.emptyValue === ""
? field.emptyValue
@@ -115,7 +116,11 @@ function readSubmittedValues(
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 (typeof value === "string") {
values[field.name] = value;
} else if (field.type === "checkbox" && !field.allowUnchanged) {
values[field.name] = UNCHECKED_CHECKBOX_VALUE;
}
}
if (requiresReason) {
const reason = formData.get("reason");
@@ -247,8 +252,9 @@ export function ContentCommandForm({
name={field.name}
type="checkbox"
defaultChecked={
retainedValue === "on" ||
(retainedValue === undefined && field.defaultValue === true)
retainedValue === undefined
? field.defaultValue === true
: retainedValue === "on"
}
{...fieldErrorProps}
/>{" "}
@@ -20,6 +20,22 @@ import { ContentHelpPage } from "./help";
import { ContentLocalizationPage } from "./localization";
import { ContentMediaPage } from "./media";
const { actionState } = vi.hoisted(() => ({
actionState: { current: null as unknown },
}));
vi.mock("react", async (importOriginal) => {
const actual = await importOriginal<typeof import("react")>();
return {
...actual,
useActionState: (_action: unknown, initialState: unknown) => [
actionState.current ?? initialState,
vi.fn(),
false,
],
};
});
vi.mock("@/actions/housekeeping-command", () => ({
executeHousekeepingCommand: vi.fn(),
}));
@@ -125,7 +141,10 @@ describe.each(cases)("Content %s page", (kind, Component, editPermission) => {
});
describe("Content actionable form wiring", () => {
beforeEach(() => vi.clearAllMocks());
beforeEach(() => {
vi.clearAllMocks();
actionState.current = null;
});
it("retains configured string values after validation and excludes files", async () => {
vi.mocked(executeHousekeepingCommand).mockResolvedValue(
@@ -157,6 +176,82 @@ describe("Content actionable form wiring", () => {
expect(state).not.toHaveProperty("values.cover");
});
it("keeps an unchecked default-true checkbox unchecked after a failed remount", async () => {
vi.mocked(executeHousekeepingCommand).mockResolvedValue(
fail("VALIDATION", "errors.housekeeping.validation", "poll-checkbox"),
);
const formData = new FormData();
const state = await submitContentCommandForm(
{
commandId: "content.engagement.poll.change",
input: { action: "create" },
fields: [
{
name: "showResults",
label: "Show results",
type: "checkbox",
defaultValue: true,
},
],
},
null,
formData,
);
expect(executeHousekeepingCommand).toHaveBeenCalledWith({
commandId: "content.engagement.poll.change",
input: { action: "create", showResults: false },
});
actionState.current = state;
const html = renderToStaticMarkup(
<ContentCommandForm
commandId="content.engagement.poll.change"
buttonLabel="Create poll"
input={{ action: "create" }}
fields={[
{
name: "showResults",
label: "Show results",
type: "checkbox",
defaultValue: true,
},
]}
/>,
);
actionState.current = null;
const showResults = html.match(/<input[^>]*name="showResults"[^>]*>/u)?.[0];
expect(showResults).toBeDefined();
expect(showResults).not.toContain("checked");
});
it("retains dependency-failed string values", async () => {
vi.mocked(executeHousekeepingCommand).mockResolvedValue(
fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
"poll-dependency",
),
);
const formData = new FormData();
formData.set("title", "Draft title");
const state = await submitContentCommandForm(
{
commandId: "content.engagement.poll.change",
input: { action: "create" },
fields: [{ name: "title", label: "Title", type: "text" }],
},
null,
formData,
);
expect(state).toMatchObject({
result: { ok: false, error: { code: "DEPENDENCY_UNAVAILABLE" } },
values: { title: "Draft title" },
});
});
it("retains submitted fields and reason after a conflict", async () => {
vi.mocked(executeHousekeepingCommand).mockResolvedValue(
fail("CONFLICT", "errors.housekeeping.conflict", "poll-conflict"),