diff --git a/app/(app)/create/components/FinalReviewChipEditModal.tsx b/app/(app)/create/components/FinalReviewChipEditModal.tsx index 3d4421c..9443807 100644 --- a/app/(app)/create/components/FinalReviewChipEditModal.tsx +++ b/app/(app)/create/components/FinalReviewChipEditModal.tsx @@ -66,6 +66,7 @@ import { uploadCreateFlowFile } from "../../../../lib/create/uploadToServer"; import CustomMethodCardWizard from "./CustomMethodCardWizard"; import { duplicateCoreValueChipInDraft, + MAX_SELECTED_CORE_VALUES, removeCoreValueChipFromDraft, } from "../../../../lib/create/coreValueChipFacet"; import { @@ -650,7 +651,7 @@ export function FinalReviewChipEditModal({ if ((state.editingPublishedRuleId?.trim() ?? "") !== "") { return; } - if ((state.selectedCoreValueIds ?? []).length >= 5) { + if ((state.selectedCoreValueIds ?? []).length >= MAX_SELECTED_CORE_VALUES) { return; } if ( @@ -725,7 +726,7 @@ export function FinalReviewChipEditModal({ onCustomize: handleCustomize, onDuplicate: (state.editingPublishedRuleId?.trim() ?? "") !== "" || - (state.selectedCoreValueIds ?? []).length >= 5 + (state.selectedCoreValueIds ?? []).length >= MAX_SELECTED_CORE_VALUES ? undefined : handleDuplicateCoreValue, }); @@ -775,8 +776,12 @@ export function FinalReviewChipEditModal({ return; } onInteract?.(); - const existingBlocks = + const persistedBlocks = state.customMethodCardFieldBlocksById?.[target.overrideKey]; + const existingBlocks = + draftFieldBlocks && draftFieldBlocks.length > 0 + ? draftFieldBlocks + : persistedBlocks; onSave({ groupKey: "coreValues", overrideKey: target.overrideKey, diff --git a/tests/components/FinalReviewPage.test.tsx b/tests/components/FinalReviewPage.test.tsx index aea63f8..25b9237 100644 --- a/tests/components/FinalReviewPage.test.tsx +++ b/tests/components/FinalReviewPage.test.tsx @@ -10,6 +10,7 @@ import { import "@testing-library/jest-dom/vitest"; import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalReviewScreen"; import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext"; +import { clearCoreValueDetailsLocalStorage } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage"; import type { CreateFlowState } from "../../app/(app)/create/types"; function catalogChipName(label: string) { @@ -363,6 +364,173 @@ describe("FinalReviewScreen — chip detail modal", () => { ).not.toBeInTheDocument(); }); + it("offers Duplicate with five values picked and hides it at six", async () => { + function CoreValuesHarness({ count }: { count: number }) { + const { replaceState } = useCreateFlow(); + useLayoutEffect(() => { + const ids = Array.from({ length: count }, (_, i) => String(i + 1)); + replaceState({ + selectedCoreValueIds: ids, + coreValuesChipsSnapshot: ids.map((id, i) => ({ + id, + label: `Value ${i + 1}`, + state: "selected" as const, + })), + }); + }, [count, replaceState]); + return ; + } + + const { unmount } = render(); + fireEvent.click( + await screen.findByRole("button", { name: catalogChipName("Value 1") }), + ); + fireEvent.click( + within(await screen.findByRole("dialog")).getByRole("button", { + name: /more options/i, + }), + ); + expect( + await screen.findByRole("menuitem", { name: /^duplicate$/i }), + ).toBeInTheDocument(); + unmount(); + + render(); + fireEvent.click( + await screen.findByRole("button", { name: catalogChipName("Value 1") }), + ); + fireEvent.click( + within(await screen.findByRole("dialog")).getByRole("button", { + name: /more options/i, + }), + ); + await waitFor(() => { + expect( + screen.getByRole("menuitem", { name: /^customize$/i }), + ).toBeInTheDocument(); + }); + expect( + screen.queryByRole("menuitem", { name: /^duplicate$/i }), + ).not.toBeInTheDocument(); + }); + + it("duplicates a fifth value into the remaining slot", async () => { + let latest: CreateFlowState = {}; + render( + { + latest = s; + }} + initial={{ + selectedCoreValueIds: ["1", "2", "3", "4", "5"], + coreValuesChipsSnapshot: ["1", "2", "3", "4", "5"].map((id, i) => ({ + id, + label: `Value ${i + 1}`, + state: "selected" as const, + })), + }} + />, + ); + fireEvent.click( + await screen.findByRole("button", { name: catalogChipName("Value 1") }), + ); + fireEvent.click( + within(await screen.findByRole("dialog")).getByRole("button", { + name: /more options/i, + }), + ); + fireEvent.click( + await screen.findByRole("menuitem", { name: /^duplicate$/i }), + ); + await waitFor(() => { + expect(latest.selectedCoreValueIds).toHaveLength(6); + }); + expect(latest.coreValuesChipsSnapshot?.map((row) => row.label)).toContain( + "Value 1 (copy)", + ); + }); + + it("saves in-modal edits to extra value field blocks", async () => { + let latest: CreateFlowState = {}; + let view: ReturnType | undefined; + try { + view = render( + { + latest = s; + }} + initial={{ + selectedCoreValueIds: ["1"], + coreValuesChipsSnapshot: [ + { id: "1", label: "Accessibility", state: "selected" }, + ], + coreValueDetailsByChipId: { + "1": { + meaning: "Everyone can participate.", + signals: "Captions and ramps.", + }, + }, + customMethodCardFieldBlocksById: { + "1": [ + { + kind: "text", + id: "extra-note", + blockTitle: "House note", + placeholderText: "Be kind.", + }, + { + kind: "text", + id: "facet-meaning", + blockTitle: "What this value means", + placeholderText: "Everyone can participate.", + }, + { + kind: "text", + id: "facet-signals", + blockTitle: "Signals", + placeholderText: "Captions and ramps.", + }, + ], + }, + }} + />, + ); + + fireEvent.click( + await screen.findByRole("button", { + name: catalogChipName("Accessibility"), + }), + ); + const dialog = await screen.findByRole("dialog"); + fireEvent.change( + within(dialog).getByRole("textbox", { name: "House note" }), + { target: { value: "Leave the ramp clear." } }, + ); + const save = within(dialog).getByRole("button", { name: "Save" }); + await waitFor(() => { + expect(save).toBeEnabled(); + }); + fireEvent.click(save); + + await waitFor(() => { + expect( + latest.customMethodCardFieldBlocksById?.["1"]?.map( + (block) => block.id, + ), + ).toEqual(["extra-note", "facet-meaning", "facet-signals"]); + }); + const extra = latest.customMethodCardFieldBlocksById?.["1"]?.find( + (block) => block.id === "extra-note", + ); + expect(extra?.kind === "text" ? extra.placeholderText : "").toBe( + "Leave the ramp clear.", + ); + } finally { + view?.unmount(); + clearCoreValueDetailsLocalStorage(); + } + }); + it("values Customize starts on the policy name step", async () => { function CoreValuesHarness() { const { replaceState } = useCreateFlow();