diff --git a/editor.planx.uk/src/@planx/components/SetValue/utils.ts b/editor.planx.uk/src/@planx/components/SetValue/utils.ts new file mode 100644 index 0000000000..67ca7ea586 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/SetValue/utils.ts @@ -0,0 +1,92 @@ +import { Store } from "pages/FlowEditor/lib/store"; +import { SetValue } from "./model"; + +interface HandleSetValue { + nodeData: SetValue; + previousValues: string | string[] | undefined; + passport: Store.passport; +} + +/** + * Handle modifying passport values when passing through a SetValue component + * Called by computePassport() + */ +export const handleSetValue = ({ + nodeData: { operation, fn, val: current }, + previousValues, + passport, +}: HandleSetValue): Store.passport => { + // We do not amend values set at objects + // These are internal exceptions we do not want to allow users to edit + // e.g. property.boundary.title + const isObject = + typeof previousValues === "object" && + !Array.isArray(previousValues) && + previousValues !== null; + if (isObject) return passport; + + const previous = formatPreviousValues(previousValues); + + const newValues = calculateNewValues({ + operation, + previous, + current, + }); + + if (newValues) { + passport.data![fn] = newValues; + + // Operation has cleared passport value + if (!newValues.length) delete passport.data![fn]; + } + + return passport; +}; + +interface CalculateNewValues { + operation: SetValue["operation"]; + previous: string | string[]; + current: string; +} + +const calculateNewValues = ({ + operation, + previous, + current, +}: CalculateNewValues): undefined | string[] => { + switch (operation) { + case "replace": + // Default behaviour when assigning passport variables + // No custom logic needed + break; + + case "removeOne": { + if (Array.isArray(previous)) { + const removeCurrent = (val: string) => val !== current; + const filtered = previous.filter(removeCurrent); + return filtered; + } + + if (previous === current) { + return []; + } + + break; + } + + case "removeAll": + return []; + + case "append": { + const combined = [...previous, current]; + const unique = [...new Set(combined)]; + return unique; + } + } +} + +const formatPreviousValues = (value: HandleSetValue["previousValues"]): string[] => { + if (!value) return []; + if (Array.isArray(value)) return value; + return [value]; +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/setValue.test.ts b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/setValue.test.ts index 1e86bba175..edc181c9da 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/setValue.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/setValue.test.ts @@ -66,10 +66,10 @@ describe("SetValue component", () => { it("sets a value if not previously set", () => { // Value not set - expect(computePassport()?.data?.myKey1).not.toBeDefined(); + expect(computePassport()?.data?.myKey).not.toBeDefined(); // Step through first SetValue - record("setValue1", { data: { myKey1: ["myFirstValue"] } }); + record("setValue1", { data: { myKey: ["myFirstValue"] } }); // SetValue is visited const breadcrumbKeys = Object.keys(getState().breadcrumbs); @@ -79,15 +79,15 @@ describe("SetValue component", () => { expect(currentCard()?.id).toEqual("middleOfService"); // Passport correctly populated - expect(computePassport()?.data?.myKey1).toHaveLength(1); - expect(computePassport()?.data?.myKey1).toContain("myFirstValue"); + expect(computePassport()?.data?.myKey).toHaveLength(1); + expect(computePassport()?.data?.myKey).toContain("myFirstValue"); }); it("replaces an existing value", () => { // Step through second SetValue - record("setValue1", { data: { myKey1: ["myFirstValue"] } }); + record("setValue1", { data: { myKey: ["myFirstValue"] } }); record("middleOfService", {}); - record("setValue2", { data: { myKey1: ["mySecondValue"] } }); + record("setValue2", { data: { myKey: ["mySecondValue"] } }); // Second SetValue is visited const breadcrumbKeys = Object.keys(getState().breadcrumbs); @@ -97,8 +97,8 @@ describe("SetValue component", () => { expect(currentCard()?.id).toEqual("endOfService"); // Passport correctly populated - expect(computePassport()?.data?.myKey1).toHaveLength(1); - expect(computePassport()?.data?.myKey1).toContain("mySecondValue"); + expect(computePassport()?.data?.myKey).toHaveLength(1); + expect(computePassport()?.data?.myKey).toContain("mySecondValue"); }); }); @@ -123,10 +123,10 @@ describe("SetValue component", () => { it("sets a value if not previously set", () => { // Value not set - expect(computePassport()?.data?.myKey1).not.toBeDefined(); + expect(computePassport()?.data?.myKey).not.toBeDefined(); // Step through first SetValue - record("setValue1", { data: { myKey1: ["myFirstValue"] } }); + record("setValue1", { data: { myKey: ["myFirstValue"] } }); // SetValue is visited const breadcrumbKeys = Object.keys(getState().breadcrumbs); @@ -136,8 +136,8 @@ describe("SetValue component", () => { expect(currentCard()?.id).toEqual("middleOfService"); // Passport correctly populated - expect(computePassport()?.data?.myKey1).toHaveLength(1); - expect(computePassport()?.data?.myKey1).toContain("myFirstValue"); + expect(computePassport()?.data?.myKey).toHaveLength(1); + expect(computePassport()?.data?.myKey).toContain("myFirstValue"); }); it("appends to an existing value", () => { diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts index 52960e0a44..11bf5305dd 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts @@ -14,7 +14,6 @@ import { SetValue } from "@planx/components/SetValue/model"; import { sortIdsDepthFirst } from "@planx/graph"; import { logger } from "airbrake"; import { objectWithoutNullishValues } from "lib/objectHelpers"; -import { castArray } from "lodash"; import difference from "lodash/difference"; import flatten from "lodash/flatten"; import isEqual from "lodash/isEqual"; @@ -30,6 +29,7 @@ import { ApplicationPath } from "./../../../../types"; import type { Store } from "."; import { NavigationStore } from "./navigation"; import type { SharedStore } from "./shared"; +import { handleSetValue } from "@planx/components/SetValue/utils"; const SUPPORTED_DECISION_TYPES = [TYPES.Checklist, TYPES.Question]; let memoizedPreviousCardId: string | undefined = undefined; @@ -259,7 +259,11 @@ export const previewStore: StateCreator< const isSetValue = flow[id].type === TYPES.SetValue; if (isSetValue) { - passport = handleSetValue(flow, id, acc, responseData, passport); + passport = handleSetValue({ + nodeData: flow[id].data as SetValue, + previous: acc.data?.[key], + passport, + }); } return passport; @@ -822,64 +826,6 @@ export const sortBreadcrumbs = ( ); }; -const handleSetValue = ( - flow: Store.flow, - id: string, - acc: Record, - responseData: Record | undefined, - passport: Store.passport, -): Store.passport => { - const { operation, fn } = flow[id]?.data as SetValue; - let previousValues = acc.data?.[fn]; - - // We do not amend values set at objects - // These are internal exceptions we do not want to allow users to edit - // e.g. property.boundary.title - const isObject = - typeof previousValues === "object" && - !Array.isArray(previousValues) && - previousValues !== null; - if (isObject) return passport; - - previousValues = formatPreviousValues(previousValues); - const currentValue = responseData?.[fn] || []; - - switch (operation) { - case "replace": - // Default behaviour when assigning passport variables - // No custom logic needed - break; - - case "removeOne": { - if (previousValues === currentValue) { - delete passport.data![fn]; - } - - if (Array.isArray(previousValues)) { - const removeCurrentValue = (val: string | number | boolean) => - val !== currentValue[0]; - const filtered = previousValues.filter(removeCurrentValue); - passport.data![fn] = filtered.length ? filtered : undefined; - } - - break; - } - - case "removeAll": - delete passport.data![fn]; - break; - - case "append": { - const combined = [...previousValues, ...currentValue]; - const uniqueValuesOnly = [...new Set(combined)]; - passport.data![fn] = uniqueValuesOnly; - break; - } - } - - return passport; -}; - function handleNodesWithPassport({ flow, id, @@ -956,12 +902,4 @@ export const removeNodesDependentOnPassport = ( return acc; }, [] as string[]); return { removedNodeIds, breadcrumbsWithoutPassportData: newBreadcrumbs }; -}; - -const formatPreviousValues = ( - value: T | T[], -): T[] => { - if (!value) return []; - if (Array.isArray(value)) return value; - return [value]; -}; +}; \ No newline at end of file