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:
@@ -97,9 +97,8 @@ export default function PatternEditor({ child, pattern, today, onCancel, onSaved
|
||||
pattern: resizePattern(weeks, cycleLength),
|
||||
};
|
||||
const changes = pattern ? changedFields(pattern, body) : body;
|
||||
const coversADay = Object.values(body.pattern).some((days) => days.length > 0);
|
||||
const canSave =
|
||||
body.name !== "" && isIsoDate(anchor) && coversADay && Object.keys(changes).length > 0 && !saving;
|
||||
// A pattern covering no day is valid (e.g. a migrated all-absent home schedule).
|
||||
const canSave = body.name !== "" && isIsoDate(anchor) && Object.keys(changes).length > 0 && !saving;
|
||||
|
||||
function save() {
|
||||
if (!canSave) return;
|
||||
|
||||
@@ -335,7 +335,7 @@ describe("ScheduleTab", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Rory" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add pattern" }));
|
||||
expect(await screen.findByRole("heading", { name: "New pattern" })).toBeTruthy();
|
||||
// Nothing named and no day covered yet.
|
||||
// Nothing named yet.
|
||||
expect(saveButton().disabled).toBe(true);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await selectMaeve();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit Alternate weekends" }));
|
||||
|
||||
Reference in New Issue
Block a user