Allow saving presence patterns that cover no weekday

The pattern editor disabled Save unless a weekday was selected, but an
all-empty weekday set is valid in the core model and the API, and
migration 9 produces one for a legacy all-absent schedule. Save now only
requires a name (plus a valid anchor date and a change to save).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 20:03:03 -04:00
co-authored by Claude Opus 5.5
parent b91ecbd5f6
commit 0d763519a0
2 changed files with 87 additions and 4 deletions
+2 -3
View File
@@ -97,9 +97,8 @@ export default function PatternEditor({ child, pattern, today, onCancel, onSaved
pattern: resizePattern(weeks, cycleLength), pattern: resizePattern(weeks, cycleLength),
}; };
const changes = pattern ? changedFields(pattern, body) : body; const changes = pattern ? changedFields(pattern, body) : body;
const coversADay = Object.values(body.pattern).some((days) => days.length > 0); // A pattern covering no day is valid (e.g. a migrated all-absent home schedule).
const canSave = const canSave = body.name !== "" && isIsoDate(anchor) && Object.keys(changes).length > 0 && !saving;
body.name !== "" && isIsoDate(anchor) && coversADay && Object.keys(changes).length > 0 && !saving;
function save() { function save() {
if (!canSave) return; if (!canSave) return;
+85 -1
View File
@@ -335,7 +335,7 @@ describe("ScheduleTab", () => {
fireEvent.click(screen.getByRole("button", { name: "Rory" })); fireEvent.click(screen.getByRole("button", { name: "Rory" }));
fireEvent.click(screen.getByRole("button", { name: "Add pattern" })); fireEvent.click(screen.getByRole("button", { name: "Add pattern" }));
expect(await screen.findByRole("heading", { name: "New pattern" })).toBeTruthy(); expect(await screen.findByRole("heading", { name: "New pattern" })).toBeTruthy();
// Nothing named and no day covered yet. // Nothing named yet.
expect(saveButton().disabled).toBe(true); expect(saveButton().disabled).toBe(true);
fireEvent.change(screen.getByLabelText("Name"), { target: { value: " Dad's weekends " } }); fireEvent.change(screen.getByLabelText("Name"), { target: { value: " Dad's weekends " } });
@@ -408,6 +408,90 @@ describe("ScheduleTab", () => {
expect(day("2026-09-15").dataset.state).toBe("away"); // Tuesday expect(day("2026-09-15").dataset.state).toBe("away"); // Tuesday
}); });
it("Save requires a name; covered days alone are not enough", async () => {
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "Rory" }));
fireEvent.click(screen.getByRole("button", { name: "Add pattern" }));
await screen.findByRole("heading", { name: "New pattern" });
fireEvent.click(within(weekGroup(1)).getByRole("button", { name: "Monday" }));
expect(saveButton().disabled).toBe(true);
fireEvent.change(screen.getByLabelText("Name"), { target: { value: " " } });
expect(saveButton().disabled).toBe(true);
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "Mondays" } });
expect(saveButton().disabled).toBe(false);
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "" } });
expect(saveButton().disabled).toBe(true);
fireEvent.click(saveButton());
expect(writes()).toEqual([]);
});
it("Add saves a pattern that covers no weekday and lists it", async () => {
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "Rory" }));
fireEvent.click(screen.getByRole("button", { name: "Add pattern" }));
await screen.findByRole("heading", { name: "New pattern" });
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "Always away" } });
fireEvent.click(screen.getByRole("button", { name: "Home" }));
fireEvent.change(screen.getByLabelText("Cycle length"), { target: { value: "2" } });
expect(saveButton().disabled).toBe(false);
fireEvent.click(saveButton());
const row = await screen.findByTestId("pattern-50");
expect(writes()).toEqual([
{
method: "POST",
path: "/api/v1/admin/children/3/presence-patterns",
body: {
name: "Always away",
kind: "home",
cycle_length_weeks: 2,
anchor_date: TODAY,
pattern: { "0": [], "1": [] },
},
},
]);
expect(findPattern(50)?.pattern).toEqual({ "0": [], "1": [] });
expect(within(row).getByText("Always away")).toBeTruthy();
expect(within(row).getByText("Home · Every 2 weeks")).toBeTruthy();
expect(within(row).getByText("Week 1: No days · Week 2: No days")).toBeTruthy();
// A home pattern covering no day: the child is away every day.
for (const date of monthDates("2026-09-01")) expect(day(date).dataset.state, date).toBe("away");
});
it("Edit of a migrated all-absent home pattern saves without touching any weekday", async () => {
const migrated: PresencePattern = {
id: 7,
child_id: 3,
name: "Home weeks",
kind: "home",
cycle_length_weeks: 2,
anchor_date: "2026-09-07",
pattern: { "0": [], "1": [] },
};
patterns[3] = [migrated];
await renderReady();
fireEvent.click(screen.getByRole("button", { name: "Rory" }));
fireEvent.click(await screen.findByRole("button", { name: "Edit Home weeks" }));
await screen.findByRole("heading", { name: "Edit pattern" });
for (const week of [1, 2]) {
for (const button of within(weekGroup(week)).getAllByRole("button")) {
expect(button.getAttribute("aria-pressed")).toBe("false");
}
}
// Unchanged: nothing to save.
expect(saveButton().disabled).toBe(true);
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "Never home" } });
expect(saveButton().disabled).toBe(false);
fireEvent.click(saveButton());
await waitFor(() => expect(screen.queryByRole("heading", { name: "Edit pattern" })).toBeNull());
expect(writes()).toEqual([{ method: "PATCH", path: `${PATTERNS_PATH}/7`, body: { name: "Never home" } }]);
expect(findPattern(7)).toEqual({ ...migrated, name: "Never home" });
const row = await screen.findByTestId("pattern-7");
expect(within(row).getByText("Week 1: No days · Week 2: No days")).toBeTruthy();
});
it("Edit PATCHes only the changed fields to /presence-patterns/{id} and refreshes", async () => { it("Edit PATCHes only the changed fields to /presence-patterns/{id} and refreshes", async () => {
await selectMaeve(); await selectMaeve();
fireEvent.click(screen.getByRole("button", { name: "Edit Alternate weekends" })); fireEvent.click(screen.getByRole("button", { name: "Edit Alternate weekends" }));