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 <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,7 @@ import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizar
|
|||||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
import {
|
import {
|
||||||
CUSTOM_RULE_FACET_BY_GROUP,
|
CUSTOM_RULE_FACET_BY_GROUP,
|
||||||
|
isCatalogMethodCardId,
|
||||||
type TemplateFacetGroupKey,
|
type TemplateFacetGroupKey,
|
||||||
} from "../../../../lib/create/customRuleFacets";
|
} from "../../../../lib/create/customRuleFacets";
|
||||||
import type { MethodFacetGroupKey } from "../../../../lib/create/removeMethodCardFromFacetSelection";
|
import type { MethodFacetGroupKey } from "../../../../lib/create/removeMethodCardFromFacetSelection";
|
||||||
@@ -378,10 +379,11 @@ export function FinalReviewChipEditModal({
|
|||||||
modalEditUnlocked: true,
|
modalEditUnlocked: true,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
}) ||
|
}) ||
|
||||||
isCustomMethodCardId(
|
(isCustomMethodCardId(
|
||||||
target.overrideKey,
|
target.overrideKey,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
)),
|
) &&
|
||||||
|
!isCatalogMethodCardId(target.overrideKey))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleWizardFieldBlocksChange = useCallback(
|
const handleWizardFieldBlocksChange = useCallback(
|
||||||
|
|||||||
@@ -213,6 +213,17 @@ export function readMethodPresetsForFacetGroup(
|
|||||||
return readMethodsArray(METHOD_MESSAGES[groupKey]);
|
return readMethodsArray(METHOD_MESSAGES[groupKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CATALOG_METHOD_CARD_IDS: ReadonlySet<string> = 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(
|
export function assignTemplateMethodSlugsToPrefill(
|
||||||
prefill: Partial<CreateFlowState>,
|
prefill: Partial<CreateFlowState>,
|
||||||
normalizedCategoryKey: string,
|
normalizedCategoryKey: string,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||||
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
|
||||||
|
import { isCatalogMethodCardId } from "./customRuleFacets";
|
||||||
import { isCustomMethodCardId } from "./isCustomMethodCardId";
|
import { isCustomMethodCardId } from "./isCustomMethodCardId";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,11 +18,17 @@ import { isCustomMethodCardId } from "./isCustomMethodCardId";
|
|||||||
* Non-empty **draft** blocks also win, including catalog ids after Customize
|
* Non-empty **draft** blocks also win, including catalog ids after Customize
|
||||||
* Finalize (facet editors ignore array order).
|
* Finalize (facet editors ignore array order).
|
||||||
*
|
*
|
||||||
* **View mode** (`modalEditUnlocked` false): when the custom card still has facet copy
|
* Catalog slugs (`video-meetings`, `signal`, …) may also have a meta row after
|
||||||
* that matches preset seeds only (see `./methodCardFacetMatchesPresetForId`), route to
|
* Customize writes a title override. Those cards keep the facet section editors
|
||||||
* {@link CustomMethodCardModalBody} so meta-only wizard cards show policy copy instead
|
* unless field blocks exist — matching catalog copy must not hide Core Principle /
|
||||||
* of empty preset section editors. Pass `customFacetDetailsMatchPreset: false` when the
|
* Logistics / etc.
|
||||||
* caller knows facet details were edited or cloned from a filled preset.
|
*
|
||||||
|
* **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: {
|
export function usesWizardFieldBlocksModalBody(args: {
|
||||||
methodId: string;
|
methodId: string;
|
||||||
@@ -45,6 +52,9 @@ export function usesWizardFieldBlocksModalBody(args: {
|
|||||||
if (!isCustomMethodCardId(args.methodId, args.meta)) {
|
if (!isCustomMethodCardId(args.methodId, args.meta)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isCatalogMethodCardId(args.methodId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
!args.modalEditUnlocked && args.customFacetDetailsMatchPreset === true
|
!args.modalEditUnlocked && args.customFacetDetailsMatchPreset === true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import "@testing-library/jest-dom/vitest";
|
|||||||
import { CommunicationMethodsScreen } from "../../app/(app)/create/screens/card/CommunicationMethodsScreen";
|
import { CommunicationMethodsScreen } from "../../app/(app)/create/screens/card/CommunicationMethodsScreen";
|
||||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||||
|
import { communicationPresetFor } from "../../lib/create/finalReviewChipPresets";
|
||||||
|
import messages from "../../messages/en/index";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -158,6 +160,48 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
|||||||
expect(textareas[2].value).toBe("Saved coc");
|
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(
|
||||||
|
<ScreenWithStateProbe
|
||||||
|
onState={() => {
|
||||||
|
/* 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 () => {
|
it("opens meaning fields editable without Customize", async () => {
|
||||||
render(
|
render(
|
||||||
<ScreenWithStateProbe
|
<ScreenWithStateProbe
|
||||||
|
|||||||
@@ -214,6 +214,39 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
).toBeGreaterThanOrEqual(1);
|
).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps catalog section editors when a title override exists and there are no custom fields", async () => {
|
||||||
|
render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={() => {
|
||||||
|
/* 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 () => {
|
it("method chip modal kebab offers Customize but not Duplicate", async () => {
|
||||||
render(<FinalReviewWithCustomizeSelections />);
|
render(<FinalReviewWithCustomizeSelections />);
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createFlowStepForCustomRuleFacetGroup,
|
createFlowStepForCustomRuleFacetGroup,
|
||||||
CUSTOM_RULE_FACETS,
|
CUSTOM_RULE_FACETS,
|
||||||
CUSTOM_RULE_FACET_BY_GROUP,
|
CUSTOM_RULE_FACET_BY_GROUP,
|
||||||
|
isCatalogMethodCardId,
|
||||||
METHOD_FACET_API_SECTION_IDS,
|
METHOD_FACET_API_SECTION_IDS,
|
||||||
PUBLISHED_CUSTOM_RULE_SELECTION_KEYS,
|
PUBLISHED_CUSTOM_RULE_SELECTION_KEYS,
|
||||||
readMethodPresetsForFacetGroup,
|
readMethodPresetsForFacetGroup,
|
||||||
@@ -75,4 +76,12 @@ describe("customRuleFacets (CR-92)", () => {
|
|||||||
expect(m.length).toBeGreaterThan(0);
|
expect(m.length).toBeGreaterThan(0);
|
||||||
expect(typeof m[0]!.id).toBe("string");
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,4 +125,40 @@ describe("usesWizardFieldBlocksModalBody", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(true);
|
).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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user