Skip to content

Commit

Permalink
refactor: Move from store, make more testable
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed May 2, 2024
1 parent 3f4d2ec commit 5e1ae08
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 81 deletions.
92 changes: 92 additions & 0 deletions editor.planx.uk/src/@planx/components/SetValue/utils.ts
Original file line number Diff line number Diff line change
@@ -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];
};
24 changes: 12 additions & 12 deletions editor.planx.uk/src/pages/FlowEditor/lib/__tests__/setValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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");
});
});

Expand All @@ -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);
Expand All @@ -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", () => {
Expand Down
76 changes: 7 additions & 69 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -822,64 +826,6 @@ export const sortBreadcrumbs = (
);
};

const handleSetValue = (
flow: Store.flow,
id: string,
acc: Record<string, any>,
responseData: Record<string, any> | 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,
Expand Down Expand Up @@ -956,12 +902,4 @@ export const removeNodesDependentOnPassport = (
return acc;
}, [] as string[]);
return { removedNodeIds, breadcrumbsWithoutPassportData: newBreadcrumbs };
};

const formatPreviousValues = <T extends string | number | boolean>(
value: T | T[],
): T[] => {
if (!value) return [];
if (Array.isArray(value)) return value;
return [value];
};
};

0 comments on commit 5e1ae08

Please sign in to comment.