Start Add value in the empty custom-policy wizard so Finalize can save a selected chip named from the title.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-21 16:58:52 -06:00
co-authored by Cursor
parent e416f37940
commit 38fe0e7e9b
2 changed files with 111 additions and 65 deletions
@@ -25,6 +25,8 @@ import {
MAX_SELECTED_CORE_VALUES, MAX_SELECTED_CORE_VALUES,
removeCoreValueChipFromDraft, removeCoreValueChipFromDraft,
} from "../../../../../lib/create/coreValueChipFacet"; } from "../../../../../lib/create/coreValueChipFacet";
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
import { import {
buildMethodCardWizardInitialValues, buildMethodCardWizardInitialValues,
coreValueDetailsFromWizardFieldBlocks, coreValueDetailsFromWizardFieldBlocks,
@@ -41,11 +43,13 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
* *
* - `pending` — preset chip just selected; modal opened to capture * - `pending` — preset chip just selected; modal opened to capture
* meaning/signals. Close (X) confirms, then unselects the chip. * meaning/signals. Close (X) confirms, then unselects the chip.
* - `customPending` — brand-new custom chip just created via the Add * - `customPending` — leftover inline custom-chip drafts (older sessions).
* value flow; modal opened with empty fields. Dismiss = drop the * Dismiss = drop the chip entirely.
* chip entirely (it was never confirmed via the Add Value button).
* - `editing` — chip already exists & is selected; modal reopened to * - `editing` — chip already exists & is selected; modal reopened to
* tweak meaning/signals. Dismiss = no-op (chip stays as-is). * 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"; type ModalSession = "pending" | "customPending" | "editing";
@@ -489,12 +493,57 @@ export function CoreValuesSelectScreen() {
description: string; description: string;
fieldBlocks: CustomMethodCardFieldBlock[]; fieldBlocks: CustomMethodCardFieldBlock[];
}) => { }) => {
const chipId = wizardCustomizeChipId ?? activeModalChipId;
if (!chipId) return;
markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null;
const trimmedTitle = title.trim(); const trimmedTitle = title.trim();
const trimmedDescription = description.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 = { const nextDetails = {
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft), ...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft),
...(trimmedDescription.length > 0 ...(trimmedDescription.length > 0
@@ -503,8 +552,8 @@ export function CoreValuesSelectScreen() {
}; };
replaceState((prev) => { replaceState((prev) => {
const snap = [...(prev.coreValuesChipsSnapshot ?? [])]; const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
const i = snap.findIndex((r) => r.id === chipId); const i = snap.findIndex((r) => r.id === existingId);
if (i >= 0 && trimmedTitle.length > 0) { if (i >= 0) {
snap[i] = { ...snap[i], label: trimmedTitle }; snap[i] = { ...snap[i], label: trimmedTitle };
} }
return { return {
@@ -512,11 +561,11 @@ export function CoreValuesSelectScreen() {
coreValuesChipsSnapshot: snap, coreValuesChipsSnapshot: snap,
coreValueDetailsByChipId: { coreValueDetailsByChipId: {
...(prev.coreValueDetailsByChipId ?? {}), ...(prev.coreValueDetailsByChipId ?? {}),
[chipId]: nextDetails, [existingId]: nextDetails,
}, },
customMethodCardFieldBlocksById: { customMethodCardFieldBlocksById: {
...(prev.customMethodCardFieldBlocksById ?? {}), ...(prev.customMethodCardFieldBlocksById ?? {}),
[chipId]: structuredClone(fieldBlocks), [existingId]: structuredClone(fieldBlocks),
}, },
}; };
}); });
@@ -526,13 +575,7 @@ export function CoreValuesSelectScreen() {
setWizardCustomizeChipId(null); setWizardCustomizeChipId(null);
setWizardInitialValues(null); setWizardInitialValues(null);
}, },
[ [draft, markCreateFlowInteraction, replaceState, wizardCustomizeChipId],
activeModalChipId,
draft,
markCreateFlowInteraction,
replaceState,
wizardCustomizeChipId,
],
); );
const kebabMenuItems = useMemo(() => { const kebabMenuItems = useMemo(() => {
@@ -587,15 +630,14 @@ export function CoreValuesSelectScreen() {
const addHandlers = { const addHandlers = {
onAddClick: () => { onAddClick: () => {
const selectedCount = coreValueOptions.filter(
(o) => o.state === "selected",
).length;
if (selectedCount >= MAX_CORE_VALUES) return;
markCreateFlowInteraction(); markCreateFlowInteraction();
setCoreValueOptions((prev) => { setWizardCustomizeChipId(null);
const next: ChipOption[] = [ setWizardInitialValues(null);
...prev, setAddCustomWizardOpen(true);
{ id: crypto.randomUUID(), label: "", state: "custom" },
];
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
});
}, },
onCustomChipConfirm: (chipId: string, value: string) => { onCustomChipConfirm: (chipId: string, value: string) => {
markCreateFlowInteraction(); markCreateFlowInteraction();
@@ -256,25 +256,12 @@ describe("CoreValuesSelectScreen", () => {
expect(labels[1]).toMatch(/What does this value mean to your group/); expect(labels[1]).toMatch(/What does this value mean to your group/);
}); });
// The "Add value" → custom-chip → modal flow uses a `customPending` // The "Add value" control opens an empty custom-policy wizard. Dismissing
// session: dismissing the modal must drop the brand-new chip entirely // without Finalize leaves the chip list unchanged. Finalize writes a
// (not just unselect it), because the user never confirmed it via // selected chip named from the wizard title.
// the modal's Add Value button. Clicking Add Value keeps the chip describe("Add value wizard", () => {
// 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).
const CUSTOM_LABEL = "ZZTopBespokeValue"; 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 * 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 * is open, and as the chip's "Remove" button aria-label. Scope to
@@ -290,33 +277,50 @@ describe("CoreValuesSelectScreen", () => {
).length; ).length;
} }
it("removes the custom chip when its modal is dismissed without Add Value", async () => { async function completeAddValueWizard(label: string) {
renderWithProviders(<CoreValuesSelectScreen />); fireEvent.click(screen.getByRole("button", { name: "Add value" }));
await addCustomChipNamed(CUSTOM_LABEL); const nameInput = await screen.findByPlaceholderText("Policy name");
expect(countCustomChips(CUSTOM_LABEL)).toBe(1); 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" }); it("opens the empty custom-policy wizard from Add value", async () => {
await waitFor(() => { renderWithProviders(<CoreValuesSelectScreen />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Add value" }));
}); expect(
// Chip must be gone — not just unselected. If it were merely await screen.findByPlaceholderText("Policy name"),
// unselected the chip button would still render. Wrap in waitFor ).toHaveValue("");
// because the chip removal flushes through `updateState` → expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument();
// `useEffect` → `setCoreValueOptions` and isn't synchronous with
// the dialog close.
await waitFor(() => {
expect(countCustomChips(CUSTOM_LABEL)).toBe(0);
});
}); });
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(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
const dialog = await addCustomChipNamed(CUSTOM_LABEL); fireEvent.click(screen.getByRole("button", { name: "Add value" }));
fireEvent.click( await screen.findByPlaceholderText("Policy name");
within(dialog).getByRole("button", { name: "Add Value" }), fireEvent.keyDown(document, { key: "Escape" });
);
await waitFor(() => { 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(<CoreValuesSelectScreen />);
await completeAddValueWizard(CUSTOM_LABEL);
await waitFor(() => {
expect(
screen.queryByPlaceholderText("Policy name"),
).not.toBeInTheDocument();
}); });
expect(countCustomChips(CUSTOM_LABEL)).toBe(1); expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
}); });