From 38fe0e7e9ba755ace84d6b868578fda04c5d808a Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:58:52 -0600 Subject: [PATCH] Start Add value in the empty custom-policy wizard so Finalize can save a selected chip named from the title. Co-authored-by: Cursor --- .../screens/select/CoreValuesSelectScreen.tsx | 94 ++++++++++++++----- .../CoreValuesSelectScreen.test.tsx | 82 ++++++++-------- 2 files changed, 111 insertions(+), 65 deletions(-) diff --git a/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx b/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx index 5732c74..743c044 100644 --- a/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx +++ b/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx @@ -25,6 +25,8 @@ import { MAX_SELECTED_CORE_VALUES, removeCoreValueChipFromDraft, } from "../../../../../lib/create/coreValueChipFacet"; +import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch"; +import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder"; import { buildMethodCardWizardInitialValues, coreValueDetailsFromWizardFieldBlocks, @@ -41,11 +43,13 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES; * * - `pending` — preset chip just selected; modal opened to capture * meaning/signals. Close (X) confirms, then unselects the chip. - * - `customPending` — brand-new custom chip just created via the Add - * value flow; modal opened with empty fields. Dismiss = drop the - * chip entirely (it was never confirmed via the Add Value button). + * - `customPending` — leftover inline custom-chip drafts (older sessions). + * Dismiss = drop the chip entirely. * - `editing` — chip already exists & is selected; modal reopened to * tweak meaning/signals. Dismiss = no-op (chip stays as-is). + * + * **Add value** (and the header "add" link) opens an empty custom-policy + * wizard. Finalize writes a selected chip named from the wizard title. */ type ModalSession = "pending" | "customPending" | "editing"; @@ -489,12 +493,57 @@ export function CoreValuesSelectScreen() { description: string; fieldBlocks: CustomMethodCardFieldBlock[]; }) => { - const chipId = wizardCustomizeChipId ?? activeModalChipId; - if (!chipId) return; - markCreateFlowInteraction(); - pendingEphemeralCoreDuplicateRef.current = null; const trimmedTitle = title.trim(); const trimmedDescription = description.trim(); + if (!trimmedTitle) return; + markCreateFlowInteraction(); + pendingEphemeralCoreDuplicateRef.current = null; + + const existingId = wizardCustomizeChipId; + if (!existingId) { + const id = crypto.randomUUID(); + const nextDetails = { + ...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, EMPTY_DETAIL), + ...(trimmedDescription.length > 0 + ? { supportText: trimmedDescription } + : {}), + }; + replaceState((prev) => { + const sel = prev.selectedCoreValueIds ?? []; + if (sel.length >= MAX_CORE_VALUES) { + return prev; + } + const snap = [...(prev.coreValuesChipsSnapshot ?? [])]; + snap.push({ + id, + label: trimmedTitle, + state: "selected", + }); + return { + ...prev, + selectedCoreValueIds: moveFacetSelectionIdToFront(sel, id), + coreValuesChipsSnapshot: snap, + coreValueDetailsByChipId: { + ...(prev.coreValueDetailsByChipId ?? {}), + [id]: nextDetails, + }, + customMethodCardFieldBlocksById: { + ...(prev.customMethodCardFieldBlocksById ?? {}), + [id]: structuredClone(fieldBlocks), + }, + customMethodCardMetaById: methodCardMetaWithCustomizeHeader( + prev.customMethodCardMetaById, + id, + { title: trimmedTitle, description: trimmedDescription }, + ), + }; + }); + setAddCustomWizardOpen(false); + setWizardCustomizeChipId(null); + setWizardInitialValues(null); + return; + } + const nextDetails = { ...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft), ...(trimmedDescription.length > 0 @@ -503,8 +552,8 @@ export function CoreValuesSelectScreen() { }; replaceState((prev) => { const snap = [...(prev.coreValuesChipsSnapshot ?? [])]; - const i = snap.findIndex((r) => r.id === chipId); - if (i >= 0 && trimmedTitle.length > 0) { + const i = snap.findIndex((r) => r.id === existingId); + if (i >= 0) { snap[i] = { ...snap[i], label: trimmedTitle }; } return { @@ -512,11 +561,11 @@ export function CoreValuesSelectScreen() { coreValuesChipsSnapshot: snap, coreValueDetailsByChipId: { ...(prev.coreValueDetailsByChipId ?? {}), - [chipId]: nextDetails, + [existingId]: nextDetails, }, customMethodCardFieldBlocksById: { ...(prev.customMethodCardFieldBlocksById ?? {}), - [chipId]: structuredClone(fieldBlocks), + [existingId]: structuredClone(fieldBlocks), }, }; }); @@ -526,13 +575,7 @@ export function CoreValuesSelectScreen() { setWizardCustomizeChipId(null); setWizardInitialValues(null); }, - [ - activeModalChipId, - draft, - markCreateFlowInteraction, - replaceState, - wizardCustomizeChipId, - ], + [draft, markCreateFlowInteraction, replaceState, wizardCustomizeChipId], ); const kebabMenuItems = useMemo(() => { @@ -587,15 +630,14 @@ export function CoreValuesSelectScreen() { const addHandlers = { onAddClick: () => { + const selectedCount = coreValueOptions.filter( + (o) => o.state === "selected", + ).length; + if (selectedCount >= MAX_CORE_VALUES) return; markCreateFlowInteraction(); - setCoreValueOptions((prev) => { - const next: ChipOption[] = [ - ...prev, - { id: crypto.randomUUID(), label: "", state: "custom" }, - ]; - queueMicrotask(() => syncCoreValuesToDraft(next)); - return next; - }); + setWizardCustomizeChipId(null); + setWizardInitialValues(null); + setAddCustomWizardOpen(true); }, onCustomChipConfirm: (chipId: string, value: string) => { markCreateFlowInteraction(); diff --git a/tests/components/CoreValuesSelectScreen.test.tsx b/tests/components/CoreValuesSelectScreen.test.tsx index fa7e044..02dce53 100644 --- a/tests/components/CoreValuesSelectScreen.test.tsx +++ b/tests/components/CoreValuesSelectScreen.test.tsx @@ -256,25 +256,12 @@ describe("CoreValuesSelectScreen", () => { expect(labels[1]).toMatch(/What does this value mean to your group/); }); - // The "Add value" → custom-chip → modal flow uses a `customPending` - // session: dismissing the modal must drop the brand-new chip entirely - // (not just unselect it), because the user never confirmed it via - // the modal's Add Value button. Clicking Add Value keeps the chip - // as a selected entry. These two tests pin both halves of the - // contract so the screens stay in sync with the create-flow draft. - describe("custom chip — confirmed vs dismissed", () => { - // Use a label guaranteed to NOT collide with any preset value - // (we'd otherwise get two matching chips and false positives). + // The "Add value" control opens an empty custom-policy wizard. Dismissing + // without Finalize leaves the chip list unchanged. Finalize writes a + // selected chip named from the wizard title. + describe("Add value wizard", () => { const CUSTOM_LABEL = "ZZTopBespokeValue"; - async function addCustomChipNamed(label: string) { - fireEvent.click(screen.getByRole("button", { name: "Add value" })); - const input = await screen.findByPlaceholderText("Type to add"); - fireEvent.change(input, { target: { value: label } }); - fireEvent.click(screen.getByRole("button", { name: "Confirm" })); - return screen.findByRole("dialog"); - } - /** * The label can also appear in the modal header while the modal * is open, and as the chip's "Remove" button aria-label. Scope to @@ -290,33 +277,50 @@ describe("CoreValuesSelectScreen", () => { ).length; } - it("removes the custom chip when its modal is dismissed without Add Value", async () => { - renderWithProviders(); - await addCustomChipNamed(CUSTOM_LABEL); - expect(countCustomChips(CUSTOM_LABEL)).toBe(1); + async function completeAddValueWizard(label: string) { + fireEvent.click(screen.getByRole("button", { name: "Add value" })); + const nameInput = await screen.findByPlaceholderText("Policy name"); + fireEvent.change(nameInput, { target: { value: label } }); + fireEvent.click(screen.getByRole("button", { name: "Next" })); + const descriptionInput = + await screen.findByPlaceholderText("Policy description"); + fireEvent.change(descriptionInput, { + target: { value: "A community-authored value." }, + }); + fireEvent.click(screen.getByRole("button", { name: "Next" })); + fireEvent.click(await screen.findByRole("button", { name: "Finalize" })); + } - fireEvent.keyDown(document, { key: "Escape" }); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - // Chip must be gone — not just unselected. If it were merely - // unselected the chip button would still render. Wrap in waitFor - // because the chip removal flushes through `updateState` → - // `useEffect` → `setCoreValueOptions` and isn't synchronous with - // the dialog close. - await waitFor(() => { - expect(countCustomChips(CUSTOM_LABEL)).toBe(0); - }); + it("opens the empty custom-policy wizard from Add value", async () => { + renderWithProviders(); + fireEvent.click(screen.getByRole("button", { name: "Add value" })); + expect( + await screen.findByPlaceholderText("Policy name"), + ).toHaveValue(""); + expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument(); }); - it("keeps the custom chip selected when Add Value is clicked", async () => { + it("leaves no chip when the Add value wizard is dismissed", async () => { renderWithProviders(); - const dialog = await addCustomChipNamed(CUSTOM_LABEL); - fireEvent.click( - within(dialog).getByRole("button", { name: "Add Value" }), - ); + fireEvent.click(screen.getByRole("button", { name: "Add value" })); + await screen.findByPlaceholderText("Policy name"); + fireEvent.keyDown(document, { key: "Escape" }); await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + expect( + screen.queryByPlaceholderText("Policy name"), + ).not.toBeInTheDocument(); + }); + expect(countCustomChips(CUSTOM_LABEL)).toBe(0); + expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument(); + }); + + it("adds a selected chip named from the wizard title after Finalize", async () => { + renderWithProviders(); + await completeAddValueWizard(CUSTOM_LABEL); + await waitFor(() => { + expect( + screen.queryByPlaceholderText("Policy name"), + ).not.toBeInTheDocument(); }); expect(countCustomChips(CUSTOM_LABEL)).toBe(1); });