From 8ff1b865fcecfeaa33956080792e31f634c2f744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 01:56:17 +0200 Subject: [PATCH] fix(settings): send a managed record's cycle flag only when it moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Modules save posted `cycleTrackingEnabled` on every submit, and the route writes it whenever it is present. An explicit boolean beats the sex-derived default in the cycle gate, so a guardian turning Labs off for a record created without a recorded sex wrote cycle OFF as a deliberate answer; setting the record's sex to FEMALE afterwards then left cycle off with nothing on screen saying why. It also listed `cycleTrackingEnabled` as changed in the record's audit trail on every save of any module. The flag now rides only when it differs from the value the form opened on, which is what the edit form's `managedProfileEditPatch` already does. The decision moved into a pure `managedModulesPatch` so a test can see it — what a form does NOT send is invisible in its markup. --- .../managed-record-settings-section.test.tsx | 62 ++++++++++++++++++- .../managed-record-settings-section.tsx | 51 ++++++++++++--- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/components/settings/__tests__/managed-record-settings-section.test.tsx b/src/components/settings/__tests__/managed-record-settings-section.test.tsx index 88f7941d0..4b58f4e43 100644 --- a/src/components/settings/__tests__/managed-record-settings-section.test.tsx +++ b/src/components/settings/__tests__/managed-record-settings-section.test.tsx @@ -5,7 +5,10 @@ import { I18nProvider } from "@/lib/i18n/context"; import { MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS } from "@/lib/record-settings"; import { ALL_METRICS } from "@/lib/validations/thresholds"; -import { ManagedRecordSettingsForm } from "../managed-record-settings-section"; +import { + managedModulesPatch, + ManagedRecordSettingsForm, +} from "../managed-record-settings-section"; const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const; @@ -201,3 +204,60 @@ describe("managed record settings forms", () => { }, ); }); + +describe("what a managed Modules save sends", () => { + const moduleKeys = Object.keys(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS); + + /** The form as it stands after somebody flips one switch. */ + const formWith = (checked: Record) => { + const form = new FormData(); + for (const [name, on] of Object.entries(checked)) { + if (on) form.set(name, "on"); + } + return form; + }; + + it("omits the cycle flag when only a direct module moved", () => { + // The record tracks cycles and the guardian is turning Labs off. Sending + // the cycle switch's position here would write the sex-derived default as + // an explicit answer and freeze it against a later change of sex. + const settings = { + modulePreferences: {}, + cycleTrackingEnabled: true, + }; + const checked = Object.fromEntries( + moduleKeys.map((key) => [key, key !== "labs"]), + ); + + const patch = managedModulesPatch( + settings, + formWith({ ...checked, cycleTrackingEnabled: true }), + ); + + expect(patch.modulePreferences.labs).toBe(false); + expect("cycleTrackingEnabled" in patch).toBe(false); + }); + + it("sends the cycle flag when the cycle switch itself moved", () => { + const patch = managedModulesPatch( + { modulePreferences: {}, cycleTrackingEnabled: true }, + formWith(Object.fromEntries(moduleKeys.map((key) => [key, true]))), + ); + + expect(patch.cycleTrackingEnabled).toBe(false); + }); + + it("sends the cycle flag when a record with none turns it on", () => { + // `cycleTrackingEnabled` absent is the record that never answered; the + // switch opens off, so turning it on is a change and has to ride. + const patch = managedModulesPatch( + { modulePreferences: {} }, + formWith({ + ...Object.fromEntries(moduleKeys.map((key) => [key, true])), + cycleTrackingEnabled: true, + }), + ); + + expect(patch.cycleTrackingEnabled).toBe(true); + }); +}); diff --git a/src/components/settings/managed-record-settings-section.tsx b/src/components/settings/managed-record-settings-section.tsx index f7970da7e..e612daae2 100644 --- a/src/components/settings/managed-record-settings-section.tsx +++ b/src/components/settings/managed-record-settings-section.tsx @@ -484,6 +484,49 @@ function ProfileSettingsForm({ ); } +/** + * What a Modules save actually sends. + * + * The direct modules ride along whether or not they moved — they are one blob + * the route merges, and a key that did not change writes the value it already + * had. `cycleTrackingEnabled` is not one of them: it is a DELEGATED module + * whose column the cycle gate honours OVER the sex-derived default, so sending + * the switch's current position on every save freezes the derivation. A record + * created with no recorded sex derives cycle OFF; a guardian who turns Labs off + * and saves would write that OFF as an explicit answer, and the record's sex + * being set to FEMALE afterwards would then leave cycle off with nothing on + * screen to say why. It also put `cycleTrackingEnabled` in the record's audit + * trail on every save of any module. + * + * So it is sent only when it differs from the value the form opened on, which + * is what the managed-record edit form's `managedProfileEditPatch` already does + * for its own five fields. + * + * Exported and pure because it is the decision this form makes, and a decision + * about what NOT to send is invisible in the rendered markup. + */ +export function managedModulesPatch( + settings: Record, + form: FormData, +): { + modulePreferences: Record; + cycleTrackingEnabled?: boolean; +} { + const cycleTrackingEnabled = form.get("cycleTrackingEnabled") === "on"; + // The same expression the switch is seeded from, so "differs from what the + // form opened on" is one statement rather than two that can drift. + const loaded = settings.cycleTrackingEnabled === true; + return { + modulePreferences: Object.fromEntries( + Object.keys(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS).map((key) => [ + key, + form.get(key) === "on", + ]), + ), + ...(cycleTrackingEnabled !== loaded ? { cycleTrackingEnabled } : {}), + }; +} + function ModulesSettingsForm({ settings, disabled, @@ -500,13 +543,7 @@ function ModulesSettingsForm({ function submit(event: FormEvent) { event.preventDefault(); - const form = new FormData(event.currentTarget); - onSave({ - modulePreferences: Object.fromEntries( - entries.map(([key]) => [key, form.get(key) === "on"]), - ), - cycleTrackingEnabled: form.get("cycleTrackingEnabled") === "on", - }); + onSave(managedModulesPatch(settings, new FormData(event.currentTarget))); } return (