From 75ba0a214faca7805fa8f3ec094ed9aab36dd1ff Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:11:21 -0600 Subject: [PATCH] Keep catalog method section editors after a Customize title override so Core Principle and related copy are not replaced by an empty custom-policy body. Co-authored-by: Cursor --- .../components/FinalReviewChipEditModal.tsx | 6 ++- lib/create/customRuleFacets.ts | 11 +++++ lib/create/usesWizardFieldBlocksModalBody.ts | 20 ++++++--- ...unicationMethodsScreenPersistence.test.tsx | 44 +++++++++++++++++++ tests/components/FinalReviewPage.test.tsx | 33 ++++++++++++++ tests/unit/customRuleFacets.test.ts | 9 ++++ .../usesWizardFieldBlocksModalBody.test.ts | 36 +++++++++++++++ 7 files changed, 152 insertions(+), 7 deletions(-) diff --git a/app/(app)/create/components/FinalReviewChipEditModal.tsx b/app/(app)/create/components/FinalReviewChipEditModal.tsx index 7ce9606..36363e0 100644 --- a/app/(app)/create/components/FinalReviewChipEditModal.tsx +++ b/app/(app)/create/components/FinalReviewChipEditModal.tsx @@ -40,6 +40,7 @@ import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizar import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks"; import { CUSTOM_RULE_FACET_BY_GROUP, + isCatalogMethodCardId, type TemplateFacetGroupKey, } from "../../../../lib/create/customRuleFacets"; import type { MethodFacetGroupKey } from "../../../../lib/create/removeMethodCardFromFacetSelection"; @@ -378,10 +379,11 @@ export function FinalReviewChipEditModal({ modalEditUnlocked: true, draftFieldBlocks, }) || - isCustomMethodCardId( + (isCustomMethodCardId( target.overrideKey, state.customMethodCardMetaById, - )), + ) && + !isCatalogMethodCardId(target.overrideKey))), ); const handleWizardFieldBlocksChange = useCallback( diff --git a/lib/create/customRuleFacets.ts b/lib/create/customRuleFacets.ts index 609b91e..e869606 100644 --- a/lib/create/customRuleFacets.ts +++ b/lib/create/customRuleFacets.ts @@ -213,6 +213,17 @@ export function readMethodPresetsForFacetGroup( return readMethodsArray(METHOD_MESSAGES[groupKey]); } +const CATALOG_METHOD_CARD_IDS: ReadonlySet = new Set( + METHOD_FACET_API_SECTION_IDS.flatMap((group) => + readMethodPresetsForFacetGroup(group).map((row) => row.id), + ), +); + +/** True when `methodId` is a shipped catalog slug, not a user-authored UUID. */ +export function isCatalogMethodCardId(methodId: string): boolean { + return CATALOG_METHOD_CARD_IDS.has(methodId); +} + export function assignTemplateMethodSlugsToPrefill( prefill: Partial, normalizedCategoryKey: string, diff --git a/lib/create/usesWizardFieldBlocksModalBody.ts b/lib/create/usesWizardFieldBlocksModalBody.ts index 0205ab0..4afc432 100644 --- a/lib/create/usesWizardFieldBlocksModalBody.ts +++ b/lib/create/usesWizardFieldBlocksModalBody.ts @@ -1,5 +1,6 @@ import type { CreateFlowState } from "../../app/(app)/create/types"; import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks"; +import { isCatalogMethodCardId } from "./customRuleFacets"; import { isCustomMethodCardId } from "./isCustomMethodCardId"; /** @@ -17,11 +18,17 @@ import { isCustomMethodCardId } from "./isCustomMethodCardId"; * Non-empty **draft** blocks also win, including catalog ids after Customize * Finalize (facet editors ignore array order). * - * **View mode** (`modalEditUnlocked` false): when the custom card still has facet copy - * that matches preset seeds only (see `./methodCardFacetMatchesPresetForId`), route to - * {@link CustomMethodCardModalBody} so meta-only wizard cards show policy copy instead - * of empty preset section editors. Pass `customFacetDetailsMatchPreset: false` when the - * caller knows facet details were edited or cloned from a filled preset. + * Catalog slugs (`video-meetings`, `signal`, …) may also have a meta row after + * Customize writes a title override. Those cards keep the facet section editors + * unless field blocks exist — matching catalog copy must not hide Core Principle / + * Logistics / etc. + * + * **View mode** (`modalEditUnlocked` false): when a **user-authored** card still has + * facet copy that matches empty preset seeds only (see + * `./methodCardFacetMatchesPresetForId`), route to {@link CustomMethodCardModalBody} + * so meta-only wizard cards show policy copy instead of empty section editors. + * Pass `customFacetDetailsMatchPreset: false` when the caller knows facet details + * were edited or cloned from a filled preset. */ export function usesWizardFieldBlocksModalBody(args: { methodId: string; @@ -45,6 +52,9 @@ export function usesWizardFieldBlocksModalBody(args: { if (!isCustomMethodCardId(args.methodId, args.meta)) { return false; } + if (isCatalogMethodCardId(args.methodId)) { + return false; + } return ( !args.modalEditUnlocked && args.customFacetDetailsMatchPreset === true ); diff --git a/tests/components/CommunicationMethodsScreenPersistence.test.tsx b/tests/components/CommunicationMethodsScreenPersistence.test.tsx index 10af42f..e9f754a 100644 --- a/tests/components/CommunicationMethodsScreenPersistence.test.tsx +++ b/tests/components/CommunicationMethodsScreenPersistence.test.tsx @@ -12,6 +12,8 @@ import "@testing-library/jest-dom/vitest"; import { CommunicationMethodsScreen } from "../../app/(app)/create/screens/card/CommunicationMethodsScreen"; import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext"; import type { CreateFlowState } from "../../app/(app)/create/types"; +import { communicationPresetFor } from "../../lib/create/finalReviewChipPresets"; +import messages from "../../messages/en/index"; afterEach(() => { cleanup(); @@ -158,6 +160,48 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => { expect(textareas[2].value).toBe("Saved coc"); }); + it("keeps catalog section editors when a title override exists and there are no custom fields", async () => { + const details = communicationPresetFor("video-meetings"); + const noFieldsHint = + messages.create.customRule.customMethodCardWizard.editModal + .noCustomFieldsYet; + render( + { + /* noop */ + }} + initial={{ + selectedCommunicationMethodIds: ["video-meetings"], + customMethodCardMetaById: { + "video-meetings": { + label: "Video Meetings", + supportText: + "Synchronous video calls for remote face-to-face interaction.", + }, + }, + communicationMethodDetailsById: { + "video-meetings": details, + }, + }} + />, + ); + + fireEvent.click( + screen.getAllByRole("button", { + name: /Video Meetings: Synchronous video/, + })[0], + ); + const dialog = await screen.findByRole("dialog"); + const textareas = within(dialog).getAllByRole( + "textbox", + ) as HTMLTextAreaElement[]; + expect(textareas).toHaveLength(3); + expect(textareas[0].value).toBe(details.corePrinciple); + expect(textareas[1].value).toBe(details.logisticsAdmin); + expect(textareas[2].value).toBe(details.codeOfConduct); + expect(within(dialog).queryByText(noFieldsHint)).not.toBeInTheDocument(); + }); + it("opens meaning fields editable without Customize", async () => { render( { ).toBeGreaterThanOrEqual(1); }); + it("keeps catalog section editors when a title override exists and there are no custom fields", async () => { + render( + { + /* noop */ + }} + initial={{ + title: "Oak Park Commons", + selectedCommunicationMethodIds: ["video-meetings"], + customMethodCardMetaById: { + "video-meetings": { + label: "Video Meetings", + supportText: + "Synchronous video calls for remote face-to-face interaction.", + }, + }, + }} + />, + ); + + fireEvent.click( + await screen.findByRole("button", { name: "Video Meetings" }), + ); + const dialog = await screen.findByRole("dialog"); + expect(within(dialog).getAllByRole("textbox")).toHaveLength(3); + expect( + within(dialog).queryByText("No custom fields yet."), + ).not.toBeInTheDocument(); + expect( + within(dialog).getByText(/core principle/i), + ).toBeInTheDocument(); + }); + it("method chip modal kebab offers Customize but not Duplicate", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Signal" })); diff --git a/tests/unit/customRuleFacets.test.ts b/tests/unit/customRuleFacets.test.ts index 9791af8..646dd17 100644 --- a/tests/unit/customRuleFacets.test.ts +++ b/tests/unit/customRuleFacets.test.ts @@ -4,6 +4,7 @@ import { createFlowStepForCustomRuleFacetGroup, CUSTOM_RULE_FACETS, CUSTOM_RULE_FACET_BY_GROUP, + isCatalogMethodCardId, METHOD_FACET_API_SECTION_IDS, PUBLISHED_CUSTOM_RULE_SELECTION_KEYS, readMethodPresetsForFacetGroup, @@ -75,4 +76,12 @@ describe("customRuleFacets (CR-92)", () => { expect(m.length).toBeGreaterThan(0); expect(typeof m[0]!.id).toBe("string"); }); + + it("isCatalogMethodCardId is true for shipped slugs and false for UUIDs", () => { + expect(isCatalogMethodCardId("video-meetings")).toBe(true); + expect(isCatalogMethodCardId("signal")).toBe(true); + expect( + isCatalogMethodCardId("550e8400-e29b-41d4-a716-446655440000"), + ).toBe(false); + }); }); diff --git a/tests/unit/usesWizardFieldBlocksModalBody.test.ts b/tests/unit/usesWizardFieldBlocksModalBody.test.ts index 74c8f3f..e770e5a 100644 --- a/tests/unit/usesWizardFieldBlocksModalBody.test.ts +++ b/tests/unit/usesWizardFieldBlocksModalBody.test.ts @@ -125,4 +125,40 @@ describe("usesWizardFieldBlocksModalBody", () => { }), ).toBe(true); }); + + it("is false for catalog ids with a title-override meta row and no field blocks", () => { + expect( + usesWizardFieldBlocksModalBody({ + methodId: "video-meetings", + meta: { + "video-meetings": { + label: "Video Meetings", + supportText: "Synchronous video calls.", + }, + }, + fieldBlocksById: { "video-meetings": [] }, + modalEditUnlocked: false, + draftFieldBlocks: null, + customFacetDetailsMatchPreset: true, + }), + ).toBe(false); + }); + + it("is true for catalog ids when persisted field blocks exist", () => { + expect( + usesWizardFieldBlocksModalBody({ + methodId: "video-meetings", + meta: { + "video-meetings": { + label: "Video Meetings", + supportText: "Synchronous video calls.", + }, + }, + fieldBlocksById: { "video-meetings": blocks }, + modalEditUnlocked: false, + draftFieldBlocks: null, + customFacetDetailsMatchPreset: true, + }), + ).toBe(true); + }); });