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 11 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.1",
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,670 changes: 1,379 additions & 1,291 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 @@ -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
53 changes: 44 additions & 9 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 info", // shows prop name "info", without content
]);
});

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 fn "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
91 changes: 74 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,115 @@ 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 = ["title", "text", "fn", "val"];
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved

// 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
}"`,
);
if (allowProps.includes(`${op.p?.[2]}`)) {
output.push(
`Updated ${node?.type ? TYPES[node.type] : "node"} ${op
.p?.[2]} from "${op.od}" to "${op.oi}"`,
);
} else {
output.push(
`Updated ${node?.type ? TYPES[node.type] : "node"} ${op.p?.[2]}`,
);
}
} 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}"`);
if (allowProps.includes(`${op.p?.[2]}`)) {
output.push(
`Added ${node?.type ? TYPES[node?.type] : "node"} ${op.p?.[2]} "${
op.oi
}"`,
);
} else {
output.push(
`Added ${node?.type ? TYPES[node?.type] : "node"} ${op.p?.[2]}`,
);
}
} 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}"`);
if (allowProps.includes(`${op.p?.[2]}`)) {
output.push(
`Removed ${node?.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${
op.od
}"`,
);
} else {
output.push(
`Removed ${node?.type ? TYPES[node.type] : "node"} ${op.p?.[2]}`,
);
}
} 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
Loading
Loading