Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: undo operations to a flow #3056

Merged
merged 15 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.2",
"@mui/lab": "5.0.0-alpha.170",
"@mui/material": "^5.15.2",
"@mui/utils": "^5.15.2",
"@opensystemslab/map": "^0.8.2",
Expand Down Expand Up @@ -57,6 +58,7 @@
"nanoid-good": "^3.1.0",
"natsort": "^2.0.3",
"navi": "^0.15.0",
"ot-json0": "^1.1.0",
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
"postcode": "^5.1.0",
"ramda": "^0.29.1",
"react": "^18.2.0",
Expand Down
2,787 changes: 1,450 additions & 1,337 deletions editor.planx.uk/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { PrivateFileUpload } from "@planx/components/shared/PrivateFileUpload/Pr
import { squareMetresToHectares } from "@planx/components/shared/utils";
import type { PublicProps } from "@planx/components/ui";
import buffer from "@turf/buffer";
import { type Feature,point } from "@turf/helpers";
import { type Feature, point } from "@turf/helpers";
import { Store, useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useRef, useState } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ const Question: React.FC<IQuestion> = (props) => {
validationSchema: object({
selected: object({
id: string().required("Select your answer before continuing"),
a: mixed().required().test(value =>
typeof value === "number" ||
typeof value === "string")
})
a: mixed()
.required()
.test(
(value) => typeof value === "number" || typeof value === "string",
),
}),
}),
});

Expand All @@ -82,9 +84,7 @@ const Question: React.FC<IQuestion> = (props) => {
}

return (
<Card
handleSubmit={formik.handleSubmit}
>
<Card handleSubmit={formik.handleSubmit}>
<QuestionHeader
title={props.text}
description={props.description}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import React, { PropsWithChildren } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import Caret from "ui/icons/Caret";

const StyledButton = styled(Button)(() => ({
const StyledButton = styled(Button, {
shouldForwardProp: (prop) => prop !== "lightFontStyle",
})<{ lightFontStyle: boolean }>(({ theme, lightFontStyle }) => ({
boxShadow: "none",
color: "black",
fontSize: "1.125rem",
fontWeight: FONT_WEIGHT_SEMI_BOLD,
color: lightFontStyle ? theme.palette.grey[600] : "black",
fontSize: lightFontStyle ? "1rem" : "1.125rem",
fontWeight: lightFontStyle ? "inherit" : FONT_WEIGHT_SEMI_BOLD,
width: "100%",
}));

Expand All @@ -20,12 +22,14 @@ interface Props {
closed: string;
};
id: string;
lightFontStyle?: boolean;
}

const SimpleExpand: React.FC<PropsWithChildren<Props>> = ({
children,
buttonText,
id,
lightFontStyle,
}) => {
const [show, setShow] = React.useState(false);
return (
Expand All @@ -35,6 +39,7 @@ const SimpleExpand: React.FC<PropsWithChildren<Props>> = ({
onClick={() => setShow(!show)}
aria-expanded={show}
aria-controls={id}
lightFontStyle={lightFontStyle || false}
>
{show ? buttonText.closed : buttonText.open}
<Caret
Expand Down
59 changes: 47 additions & 12 deletions editor.planx.uk/src/@planx/graph/__tests__/formatOps.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { formatOps,Graph } from "../index";
import { formatOps, Graph } from "../index";

describe("Update operations", () => {
test("Updating a single property of a node", () => {
test("Updating a single property of a node that is in `allowProps`", () => {
const ops = [
{
p: ["FW5G3EMBI3", "data", "text"],
Expand All @@ -11,7 +11,21 @@ describe("Update operations", () => {
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
'Updated Checklist text from "Which fruits?" to "Which vegetables?"',
'Updated Checklist text from "Which fruits?" to "Which vegetables?"', // shows prop name "fn" and content "from x to y"
]);
});

test("Updating a single property of a node that is not in `allowProps`", () => {
const ops = [
{
p: ["FW5G3EMBI3", "data", "info"],
oi: "New help text",
od: "Old help text",
},
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
`Updated Checklist help text ("Why it matters")`, // shows prop name "info", without content
]);
});

Expand All @@ -32,9 +46,9 @@ describe("Update operations", () => {
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
'Added Checklist fn "fruit"',
'Added Answer val "berry.blue"',
'Added Answer val "banana"',
'Added Checklist data field "fruit"',
'Added Answer data field "berry.blue"',
'Added Answer data field "banana"',
]);
});
});
Expand Down Expand Up @@ -118,12 +132,8 @@ describe("Insert operations", () => {
]);
});

test("Adding a new child and data property to an existing node", () => {
test("Adding a new child to an existing node", () => {
const ops = [
{
p: ["FW5G3EMBI3", "data", "description"],
oi: "<p>Fruits contain seeds and come from the flower of a plant</p>",
},
{
p: ["FW5G3EMBI3", "edges"],
oi: ["WDwUTbF7Gq", "SO5XbLwSYp", "xTBfSd1Tjy", "zzQAMXexRj"],
Expand All @@ -141,11 +151,36 @@ describe("Insert operations", () => {
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
'Added Checklist description "<p>Fruits contain seeds and come from the flower of a plant</p>"',
"Updated order of Checklist edges",
'Added Answer "Strawberry"',
]);
});

test("Adding a data property to an existing node that is in `allowProps`", () => {
const ops = [
{
p: ["FW5G3EMBI3", "data", "fn"],
oi: "food.fruit",
},
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
`Added Checklist data field "food.fruit"`, // shows prop name "fn" and content
]);
});

test("Adding a data property to an existing node that is not in `allowProps`", () => {
const ops = [
{
p: ["FW5G3EMBI3", "data", "description"],
oi: "<p>Fruits contain seeds and come from the flower of a plant</p>",
},
];

expect(formatOps(flowWithChecklist, ops)).toEqual([
"Added Checklist description", // only shows prop name "description", not content
]);
});
});

describe("Remove operations", () => {
Expand Down
111 changes: 94 additions & 17 deletions editor.planx.uk/src/@planx/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,58 +507,135 @@ export const sortIdsDepthFirst =
export const formatOps = (graph: Graph, ops: Array<OT.Op>): string[] => {
const output: string[] = [];

// Only show full change description for simple props, omit complex or long ones like `moreInfo`, `fileTypes`, etc
const allowProps: string[] = ["title", "text", "fn", "val"];

// Create a simple lookup to overwrite most common component props to a more human-readable name
const propsMap: Record<string, string> = {
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
fn: "data field",
val: "data field",
info: `help text ("Why it matters")`,
howMeasured: `help text ("How is it defined?")`,
policyRef: `help text source links`,
definitionImg: `help text image`,
};

// Updating a node or its properties (update = delete + insert)
const handleUpdate = (node: Node, op: OT.Object.Replace) => {
if (op.od.type && op.oi.type) {
if (op.od?.type && op.oi?.type) {
output.push(
`Replaced ${TYPES[op.od.type]} "${op.od.data?.title || op.od.data?.text
}" with ${TYPES[op.oi.type]} "${op.oi.data?.title || op.oi.data?.text
`Replaced ${TYPES[op.od.type]} "${
op.od.data?.title ||
op.od.data?.text ||
op.od.data?.content ||
op.od.data?.fn ||
op.od.data?.val ||
op.od.data?.flowId
}" with ${TYPES[op.oi.type]} "${
op.oi.data?.title ||
op.oi.data?.text ||
op.oi.data?.content ||
op.oi.data?.fn ||
op.oi.data?.val ||
op.od.data?.flowId
}"`,
);
} else if (op.p.includes("data")) {
output.push(
`Updated ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} from "${op.od}" to "${op.oi
}"`,
);
const prop = op.p?.[2] as string;
if (allowProps.includes(prop)) {
output.push(
`Updated ${node?.type ? TYPES[node.type] : "node"} ${
propsMap[prop] || prop
} from "${op.od}" to "${op.oi}"`,
);
} else {
output.push(
`Updated ${node?.type ? TYPES[node.type] : "node"} ${
propsMap[prop] || prop
}`,
);
}
} else if (op.p.includes("edges")) {
output.push(
`Updated order of ${node.type ? TYPES[node.type] : "graph"} edges`,
`Updated order of ${node?.type ? TYPES[node.type] : "graph"} edges`,
);
}
};

// Updating the _root list (update = list insert or list delete)
const handleRootUpdate = (op: OT.Array.Replace) => {
if (op.p.includes("edges") && op.p.includes("_root")) {
output.push(`Re-ordered the graph`);
output.push(`Re-ordered the root graph`);
} else if (op.p.includes("edges")) {
output.push(`Moved node`);
}
};

// Adding (inserting) a node or its properties
const handleAdd = (node: Node, op: OT.Object.Add) => {
if (op.oi.type) {
if (op.oi?.type) {
output.push(
`Added ${TYPES[op.oi.type]} "${op.oi.data?.title || op.oi.data?.text
`Added ${TYPES[op.oi.type]} "${
op.oi.data?.title ||
op.oi.data?.text ||
op.oi.data?.content ||
op.oi.data?.fn ||
op.oi.data?.flowId
}"`,
);
} else if (op.p.includes("data")) {
output.push(`Added ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${op.oi}"`);
const prop = op.p?.[2] as string;
if (allowProps.includes(prop)) {
output.push(
`Added ${node?.type ? TYPES[node?.type] : "node"} ${
propsMap[prop] || prop
} "${op.oi}"`,
);
} else {
output.push(
`Added ${node?.type ? TYPES[node?.type] : "node"} ${
propsMap[prop] || prop
}`,
);
}
} else if (op.p.includes("edges")) {
output.push(`Added ${node.type ? TYPES[node.type] : "node"} to branch`);
const node = graph[op.oi?.[0]];
output.push(`Added ${node?.type ? TYPES[node.type] : "node"} to branch`);
}
};

// Removing (deleting) a node or its properties
const handleRemove = (node: Node, op: OT.Object.Remove) => {
if (op.od.type) {
if (op.od?.type) {
output.push(
`Removed ${TYPES[op.od.type]} "${op.od.data?.title || op.od.data?.text
`Removed ${TYPES[op.od.type]} "${
op.od.data?.title ||
op.od.data?.text ||
op.od.data?.content ||
op.od.data?.fn ||
op.od.data?.flowId
}"`,
);
} else if (op.p.includes("data")) {
output.push(`Removed ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${op.od}"`);
const prop = op.p?.[2] as string;
if (allowProps.includes(prop)) {
output.push(
`Removed ${node?.type ? TYPES[node.type] : "node"} ${
propsMap[prop] || prop
} "${op.od}"`,
);
} else {
output.push(
`Removed ${node?.type ? TYPES[node.type] : "node"} ${
propsMap[prop] || prop
}`,
);
}
} else if (op.p.includes("edges")) {
output.push(`Removed ${node.type ? TYPES[node.type] : "node"} from branch`);
const node = graph[op.od?.[0]];
output.push(
`Removed ${node?.type ? TYPES[node.type] : "node"} from branch`,
);
}
};

Expand Down
2 changes: 1 addition & 1 deletion editor.planx.uk/src/lib/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// add/edit/remove feature flags in array below
const AVAILABLE_FEATURE_FLAGS = ["UNDO", "EDITOR_NAVIGATION"] as const;
const AVAILABLE_FEATURE_FLAGS = ["EDITOR_NAVIGATION"] as const;

type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number];

Expand Down
Loading
Loading