Add custom intervention modals
This commit is contained in:
@@ -113,4 +113,23 @@ describe("CardStack Component", () => {
|
||||
|
||||
expect(screen.getAllByText("SELECTED").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("calls onCardSelect when re-clicking an already selected card", () => {
|
||||
const onCardSelect = vi.fn();
|
||||
render(
|
||||
<CardStack
|
||||
cards={SAMPLE_CARDS}
|
||||
selectedIds={["1"]}
|
||||
onCardSelect={onCardSelect}
|
||||
title="Pick an option"
|
||||
/>,
|
||||
);
|
||||
|
||||
const cardButtons = screen.getAllByRole("button", {
|
||||
name: "Option A: Description A",
|
||||
});
|
||||
fireEvent.click(cardButtons[0]);
|
||||
expect(onCardSelect).toHaveBeenCalledWith("1");
|
||||
expect(screen.queryByRole("button", { name: "Remove policy" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,4 +124,46 @@ describe("applyFinalReviewChipEditPatch", () => {
|
||||
|
||||
expect(Object.keys(result)).toEqual(["conflictManagementDetailsById"]);
|
||||
});
|
||||
|
||||
it("merges customMethodCardFieldBlocksById when the patch carries field blocks", () => {
|
||||
const state: CreateFlowState = {
|
||||
customMethodCardFieldBlocksById: {
|
||||
other: [
|
||||
{
|
||||
kind: "text",
|
||||
id: "x",
|
||||
blockTitle: "T",
|
||||
placeholderText: "keep",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const patch: FinalReviewChipEditPatch = {
|
||||
groupKey: "communication",
|
||||
overrideKey: "550e8400-e29b-41d4-a716-446655440000",
|
||||
value: {
|
||||
corePrinciple: "a",
|
||||
logisticsAdmin: "b",
|
||||
codeOfConduct: "c",
|
||||
},
|
||||
customMethodCardFieldBlocks: [
|
||||
{
|
||||
kind: "text",
|
||||
id: "f1",
|
||||
blockTitle: "Notes",
|
||||
placeholderText: "edited",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyFinalReviewChipEditPatch(state, patch);
|
||||
|
||||
expect(result.communicationMethodDetailsById).toEqual({
|
||||
"550e8400-e29b-41d4-a716-446655440000": patch.value,
|
||||
});
|
||||
expect(result.customMethodCardFieldBlocksById).toEqual({
|
||||
other: state.customMethodCardFieldBlocksById?.other,
|
||||
"550e8400-e29b-41d4-a716-446655440000": patch.customMethodCardFieldBlocks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,19 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
expect(rows).toEqual([{ name: "Communication", chips: ["Signal"] }]);
|
||||
});
|
||||
|
||||
it("resolves user-authored method ids from customMethodCardMetaById", () => {
|
||||
const customId = "00000000-0000-4000-8000-000000000001";
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", customId],
|
||||
customMethodCardMetaById: {
|
||||
[customId]: { label: "Our Slack Ritual", supportText: "desc" },
|
||||
},
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
const comm = rows.find((r) => r.name === "Communication");
|
||||
expect(comm?.chips).toEqual(["Signal", "Our Slack Ritual"]);
|
||||
});
|
||||
|
||||
it("dedupes repeated labels from duplicate ids", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "signal"],
|
||||
|
||||
@@ -169,6 +169,25 @@ describe("buildPublishPayload — methodSelections", () => {
|
||||
expect(entries?.[0]?.blocks?.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("uses customMethodCardMetaById label when preset id is unknown", () => {
|
||||
const customId = "00000000-0000-4000-8000-000000000002";
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: [customId],
|
||||
customMethodCardMetaById: {
|
||||
[customId]: { label: "Custom Comm", supportText: "More" },
|
||||
},
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const ms = r.document.methodSelections as
|
||||
| { communication?: Array<{ id: string; label: string }> }
|
||||
| undefined;
|
||||
expect(ms?.communication?.length).toBe(1);
|
||||
expect(ms?.communication?.[0]?.id).toBe(customId);
|
||||
expect(ms?.communication?.[0]?.label).toBe("Custom Comm");
|
||||
});
|
||||
|
||||
it("emits preset-only sections when a method is selected without an override", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
|
||||
@@ -126,6 +126,40 @@ describe("createFlowStateSchema", () => {
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts customMethodCardFieldBlocksById", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
customMethodCardFieldBlocksById: {
|
||||
"card-uuid": [
|
||||
{
|
||||
kind: "text",
|
||||
id: "f1",
|
||||
blockTitle: "Notes",
|
||||
placeholderText: "Optional",
|
||||
},
|
||||
{
|
||||
kind: "badges",
|
||||
id: "f2",
|
||||
blockTitle: "Tags",
|
||||
options: ["a", "b"],
|
||||
},
|
||||
{
|
||||
kind: "upload",
|
||||
id: "f3",
|
||||
blockTitle: "Attachment",
|
||||
fileName: "doc.pdf",
|
||||
},
|
||||
{
|
||||
kind: "proportion",
|
||||
id: "f4",
|
||||
blockTitle: "Share",
|
||||
defaultPercent: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts templateReviewEntryFromCreateFlow", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
templateReviewEntryFromCreateFlow: true,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isCustomMethodCardId } from "../../lib/create/isCustomMethodCardId";
|
||||
|
||||
describe("isCustomMethodCardId", () => {
|
||||
it("is false when meta is missing or id has no entry", () => {
|
||||
expect(isCustomMethodCardId("signal", undefined)).toBe(false);
|
||||
expect(isCustomMethodCardId("signal", {})).toBe(false);
|
||||
});
|
||||
|
||||
it("is true when customMethodCardMetaById has the id", () => {
|
||||
const id = "550e8400-e29b-41d4-a716-446655440000";
|
||||
expect(
|
||||
isCustomMethodCardId(id, {
|
||||
[id]: { label: "L", supportText: "S" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mergePresetMethodsWithCustom } from "../../lib/create/mergePresetMethodsWithCustom";
|
||||
|
||||
describe("mergePresetMethodsWithCustom", () => {
|
||||
it("appends selected custom ids that have meta after presets", () => {
|
||||
const presets = [
|
||||
{ id: "a", label: "A", supportText: "sa" },
|
||||
{ id: "b", label: "B", supportText: "sb" },
|
||||
];
|
||||
const customId = "00000000-0000-4000-8000-000000000099";
|
||||
const merged = mergePresetMethodsWithCustom(
|
||||
presets,
|
||||
["b", customId, "a"],
|
||||
{
|
||||
[customId]: { label: "Custom", supportText: "cx" },
|
||||
},
|
||||
);
|
||||
expect(merged.map((m) => m.id)).toEqual(["a", "b", customId]);
|
||||
expect(merged[2]).toEqual({
|
||||
id: customId,
|
||||
label: "Custom",
|
||||
supportText: "cx",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { moveFacetSelectionIdToFront } from "../../lib/create/methodCardSelectionOrder";
|
||||
|
||||
describe("moveFacetSelectionIdToFront", () => {
|
||||
it("places a new id at index 0", () => {
|
||||
expect(moveFacetSelectionIdToFront(["a", "b"], "c")).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("moves an existing id to index 0 without duplicating", () => {
|
||||
expect(moveFacetSelectionIdToFront(["a", "b", "c"], "b")).toEqual([
|
||||
"b",
|
||||
"a",
|
||||
"c",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles empty prior selection", () => {
|
||||
expect(moveFacetSelectionIdToFront([], "x")).toEqual(["x"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
import { removeMethodCardFromFacetSelection } from "../../lib/create/removeMethodCardFromFacetSelection";
|
||||
|
||||
const CUSTOM_A = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
|
||||
describe("removeMethodCardFromFacetSelection", () => {
|
||||
it("returns {} when the card is not in the facet selection", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
};
|
||||
expect(
|
||||
removeMethodCardFromFacetSelection(state, "communication", "loomio"),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("removes a preset id, its details, and leaves other selections", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "slack"],
|
||||
communicationMethodDetailsById: {
|
||||
signal: {} as never,
|
||||
slack: {} as never,
|
||||
},
|
||||
};
|
||||
const patch = removeMethodCardFromFacetSelection(
|
||||
state,
|
||||
"communication",
|
||||
"signal",
|
||||
);
|
||||
expect(patch.selectedCommunicationMethodIds).toEqual(["slack"]);
|
||||
expect(patch.communicationMethodDetailsById).toEqual({ slack: {} });
|
||||
});
|
||||
|
||||
it("removes a custom card id and clears meta + field blocks", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", CUSTOM_A],
|
||||
communicationMethodDetailsById: {
|
||||
signal: {} as never,
|
||||
[CUSTOM_A]: {} as never,
|
||||
},
|
||||
customMethodCardMetaById: {
|
||||
[CUSTOM_A]: { label: "P", supportText: "D" },
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
[CUSTOM_A]: [],
|
||||
},
|
||||
};
|
||||
const patch = removeMethodCardFromFacetSelection(
|
||||
state,
|
||||
"communication",
|
||||
CUSTOM_A,
|
||||
);
|
||||
expect(patch.selectedCommunicationMethodIds).toEqual(["signal"]);
|
||||
expect(patch.communicationMethodDetailsById).toEqual({ signal: {} });
|
||||
expect(patch.customMethodCardMetaById).toBeUndefined();
|
||||
expect(patch.customMethodCardFieldBlocksById).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,19 @@ describe("stripCustomRuleSelectionFields", () => {
|
||||
selectedConflictManagementIds: ["z"],
|
||||
methodSectionsPinCommitted: { communication: true },
|
||||
coreValueDetailsByChipId: { "1": { meaning: "", signals: "" } },
|
||||
customMethodCardMetaById: {
|
||||
x: { label: "Custom", supportText: "S" },
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
x: [
|
||||
{
|
||||
kind: "text",
|
||||
id: "f1",
|
||||
blockTitle: "T",
|
||||
placeholderText: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [{ categoryName: "Communication", entries: [] }],
|
||||
};
|
||||
const out = stripCustomRuleSelectionFields(prev);
|
||||
@@ -28,5 +41,7 @@ describe("stripCustomRuleSelectionFields", () => {
|
||||
expect(out.selectedConflictManagementIds).toBeUndefined();
|
||||
expect(out.methodSectionsPinCommitted).toBeUndefined();
|
||||
expect(out.coreValueDetailsByChipId).toBeUndefined();
|
||||
expect(out.customMethodCardMetaById).toBeUndefined();
|
||||
expect(out.customMethodCardFieldBlocksById).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user