From f06f3bdfba05b8db7d6ce14a0419d511186d062d Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Thu, 25 Apr 2024 10:02:56 +0200 Subject: [PATCH 01/10] it works --- editor.planx.uk/package.json | 1 + editor.planx.uk/pnpm-lock.yaml | 3 ++ editor.planx.uk/src/@planx/graph/index.ts | 39 +++++++++++++------ .../src/pages/FlowEditor/index.tsx | 34 ++++++++++++---- .../src/pages/FlowEditor/lib/store/editor.ts | 9 +++++ editor.planx.uk/types.d.ts | 1 + 6 files changed, 68 insertions(+), 19 deletions(-) diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 10f52299c2..e70d64ea7e 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -57,6 +57,7 @@ "nanoid-good": "^3.1.0", "natsort": "^2.0.3", "navi": "^0.15.0", + "ot-json0": "^1.1.0", "postcode": "^5.1.0", "ramda": "^0.29.1", "react": "^18.2.0", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index 49d780b6ff..e04ca25390 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -172,6 +172,9 @@ dependencies: navi: specifier: ^0.15.0 version: 0.15.0 + ot-json0: + specifier: ^1.1.0 + version: 1.1.0 postcode: specifier: ^5.1.0 version: 5.1.0 diff --git a/editor.planx.uk/src/@planx/graph/index.ts b/editor.planx.uk/src/@planx/graph/index.ts index 36c89a7296..0b0bb75d98 100644 --- a/editor.planx.uk/src/@planx/graph/index.ts +++ b/editor.planx.uk/src/@planx/graph/index.ts @@ -509,16 +509,19 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { // 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 + }" with ${TYPES[op.oi.type]} "${ + op.oi.data?.title || op.oi.data?.text || op.oi.data?.content }"`, ); } else if (op.p.includes("data")) { output.push( - `Updated ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} from "${op.od}" to "${op.oi - }"`, + `Updated ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} from "${ + op.od + }" to "${op.oi}"`, ); } else if (op.p.includes("edges")) { output.push( @@ -536,13 +539,18 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { // 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 }"`, ); } else if (op.p.includes("data")) { - output.push(`Added ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${op.oi}"`); + output.push( + `Added ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${ + op.oi + }"`, + ); } else if (op.p.includes("edges")) { output.push(`Added ${node.type ? TYPES[node.type] : "node"} to branch`); } @@ -550,15 +558,22 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { // 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 }"`, ); } else if (op.p.includes("data")) { - output.push(`Removed ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${op.od}"`); + output.push( + `Removed ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${ + op.od + }"`, + ); } else if (op.p.includes("edges")) { - output.push(`Removed ${node.type ? TYPES[node.type] : "node"} from branch`); + output.push( + `Removed ${node.type ? TYPES[node.type] : "node"} from branch`, + ); } }; diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index 8905a3667f..a3482d4289 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -4,6 +4,7 @@ import "./floweditor.scss"; import { gql, useSubscription } from "@apollo/client"; import UndoOutlined from "@mui/icons-material/UndoOutlined"; import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; import { formatOps } from "@planx/graph"; import { OT } from "@planx/graph/types"; @@ -17,10 +18,11 @@ import PreviewBrowser from "./components/PreviewBrowser"; import { useStore } from "./lib/store"; import useScrollControlsAndRememberPosition from "./lib/useScrollControlsAndRememberPosition"; -interface Operation { - id: string; +export interface Operation { + id: number; createdAt: string; actor?: { + id: number; firstName: string; lastName: string; }; @@ -109,7 +111,12 @@ export const LastEdited = () => { }; export const EditHistory = () => { - const [flowId, flow] = useStore((state) => [state.id, state.flow]); + const [flowId, flow, canUserEditTeam, undoOperation] = useStore((state) => [ + state.id, + state.flow, + state.canUserEditTeam, + state.undoOperation, + ]); const { data, loading, error } = useSubscription<{ operations: Operation[] }>( gql` @@ -122,6 +129,7 @@ export const EditHistory = () => { id createdAt: created_at actor { + id firstName: first_name lastName: last_name } @@ -142,14 +150,14 @@ export const EditHistory = () => { } // Handle missing operations (e.g. non-production data) - if (!loading && !data?.operations) return null; + if (!loading && !data?.operations[0].actor) return null; return ( {loading && !data ? ( ) : ( - data?.operations?.map((op: Operation) => ( + data?.operations?.map((op: Operation, i: number) => ( { > - {`Edited ${formatLastEditDate(op.createdAt)}`} + {`${op.actor ? `Edited` : `Created`} ${formatLastEditDate( + op.createdAt, + )}`} {op.actor && ( @@ -173,7 +183,17 @@ export const EditHistory = () => { )} - + {i === 0 && ( + undoOperation(op)} + disabled={!canUserEditTeam} + color="primary" + > + + + )} {op.data && ( diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts index f850c66d25..811189716c 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -9,6 +9,7 @@ import { ROOT_NODE_KEY, update, } from "@planx/graph"; +import { OT } from "@planx/graph/types"; import axios from "axios"; import { client } from "lib/graphql"; import debounce from "lodash/debounce"; @@ -16,6 +17,8 @@ import isEmpty from "lodash/isEmpty"; import omitBy from "lodash/omitBy"; import { customAlphabet } from "nanoid-good"; import en from "nanoid-good/locale/en"; +import { type } from "ot-json0"; +import { Operation } from "pages/FlowEditor"; import type { StateCreator } from "zustand"; import { FlowLayout } from "../../components/Flow"; @@ -88,6 +91,7 @@ export interface EditorStore extends Store.Store { ) => Promise; removeNode: (id: Store.nodeId, parent: Store.nodeId) => void; updateNode: (node: any, relationships?: any) => void; + undoOperation: (operation: Operation) => void; } export const editorStore: StateCreator< @@ -437,4 +441,9 @@ export const editorStore: StateCreator< })(get().flow); send(ops); }, + + undoOperation: (op: Operation) => { + const inverseOps: OT.Op[] = type.invert(op.data); + send(inverseOps); + }, }); diff --git a/editor.planx.uk/types.d.ts b/editor.planx.uk/types.d.ts index 839701d0c8..53fbace268 100644 --- a/editor.planx.uk/types.d.ts +++ b/editor.planx.uk/types.d.ts @@ -9,3 +9,4 @@ declare module "draftjs-to-html"; declare module "mathjs"; declare module "@opensystemslab/map"; declare module "wkt"; +declare module "ot-json0"; From 2faeb09bf6f921193f42040f8a01fd08339ef59d Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Thu, 25 Apr 2024 12:17:18 +0200 Subject: [PATCH 02/10] account for missing actor on local envs --- editor.planx.uk/src/pages/FlowEditor/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index a3482d4289..d4e1d1b12c 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -150,7 +150,7 @@ export const EditHistory = () => { } // Handle missing operations (e.g. non-production data) - if (!loading && !data?.operations[0].actor) return null; + if (!loading && !data?.operations[0]?.actor) return null; return ( From 5c021b102a611bbec8a63d812aa384f0ee6239e1 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Thu, 25 Apr 2024 18:19:13 +0200 Subject: [PATCH 03/10] date as subheader, fix setvalue --- editor.planx.uk/src/@planx/graph/index.ts | 20 +++++++++++++++---- .../src/pages/FlowEditor/index.tsx | 16 +++++++-------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/editor.planx.uk/src/@planx/graph/index.ts b/editor.planx.uk/src/@planx/graph/index.ts index 0b0bb75d98..1fb9b0d689 100644 --- a/editor.planx.uk/src/@planx/graph/index.ts +++ b/editor.planx.uk/src/@planx/graph/index.ts @@ -512,9 +512,15 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { if (op.od?.type && op.oi?.type) { output.push( `Replaced ${TYPES[op.od.type]} "${ - op.od.data?.title || op.od.data?.text || op.od.data?.content + op.od.data?.title || + op.od.data?.text || + op.od.data?.content || + op.od.data?.fn }" with ${TYPES[op.oi.type]} "${ - op.oi.data?.title || op.oi.data?.text || op.oi.data?.content + op.oi.data?.title || + op.oi.data?.text || + op.oi.data?.content || + op.oi.data?.fn }"`, ); } else if (op.p.includes("data")) { @@ -542,7 +548,10 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { if (op.oi?.type) { output.push( `Added ${TYPES[op.oi.type]} "${ - op.oi.data?.title || op.oi.data?.text || op.oi.data?.content + op.oi.data?.title || + op.oi.data?.text || + op.oi.data?.content || + op.oi.data?.fn }"`, ); } else if (op.p.includes("data")) { @@ -561,7 +570,10 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { if (op.od?.type) { output.push( `Removed ${TYPES[op.od.type]} "${ - op.od.data?.title || op.od.data?.text || op.od.data?.content + op.od.data?.title || + op.od.data?.text || + op.od.data?.content || + op.od.data?.fn }"`, ); } else if (op.p.includes("data")) { diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index d4e1d1b12c..d9ca4f55c7 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -173,15 +173,15 @@ export const EditHistory = () => { > - {`${op.actor ? `Edited` : `Created`} ${formatLastEditDate( - op.createdAt, - )}`} + {`${ + op.actor + ? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}` + : `Created flow` + }`} + + + {formatLastEditDate(op.createdAt)} - {op.actor && ( - - {`by ${op.actor?.firstName} ${op.actor?.lastName}`} - - )} {i === 0 && ( Date: Fri, 26 Apr 2024 15:53:31 +0200 Subject: [PATCH 04/10] tidy formatting --- editor.planx.uk/src/@planx/graph/index.ts | 30 ++++++++++++------- .../src/pages/FlowEditor/index.tsx | 4 +-- .../src/pages/FlowEditor/lib/store/editor.ts | 7 ++--- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/editor.planx.uk/src/@planx/graph/index.ts b/editor.planx.uk/src/@planx/graph/index.ts index 1fb9b0d689..b6dbbb64f7 100644 --- a/editor.planx.uk/src/@planx/graph/index.ts +++ b/editor.planx.uk/src/@planx/graph/index.ts @@ -515,23 +515,25 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { op.od.data?.title || op.od.data?.text || op.od.data?.content || - op.od.data?.fn + op.od.data?.fn || + 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?.fn || + op.od.data?.flowId }"`, ); } else if (op.p.includes("data")) { output.push( - `Updated ${node.type ? TYPES[node.type] : "node"} ${op.p?.[2]} from "${ + `Updated ${node?.type ? TYPES[node.type] : "node"} ${op.p?.[2]} from "${ op.od }" to "${op.oi}"`, ); } 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`, ); } }; @@ -539,7 +541,9 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { // 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`); } }; @@ -551,17 +555,19 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { op.oi.data?.title || op.oi.data?.text || op.oi.data?.content || - op.oi.data?.fn + 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]} "${ + `Added ${node?.type ? TYPES[node?.type] : "node"} ${op.p?.[2]} "${ op.oi }"`, ); } 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`); } }; @@ -573,18 +579,20 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { op.od.data?.title || op.od.data?.text || op.od.data?.content || - op.od.data?.fn + 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]} "${ + `Removed ${node?.type ? TYPES[node.type] : "node"} ${op.p?.[2]} "${ op.od }"`, ); } else if (op.p.includes("edges")) { + const node = graph[op.od?.[0]]; output.push( - `Removed ${node.type ? TYPES[node.type] : "node"} from branch`, + `Removed ${node?.type ? TYPES[node.type] : "node"} from branch`, ); } }; diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index d9ca4f55c7..d46cbc7508 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -150,7 +150,7 @@ export const EditHistory = () => { } // Handle missing operations (e.g. non-production data) - if (!loading && !data?.operations[0]?.actor) return null; + if (!loading && !data?.operations) return null; return ( @@ -187,7 +187,7 @@ export const EditHistory = () => { undoOperation(op)} + onClick={() => undoOperation(op.data)} disabled={!canUserEditTeam} color="primary" > diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts index 811189716c..0a9eed7a61 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -18,7 +18,6 @@ import omitBy from "lodash/omitBy"; import { customAlphabet } from "nanoid-good"; import en from "nanoid-good/locale/en"; import { type } from "ot-json0"; -import { Operation } from "pages/FlowEditor"; import type { StateCreator } from "zustand"; import { FlowLayout } from "../../components/Flow"; @@ -91,7 +90,7 @@ export interface EditorStore extends Store.Store { ) => Promise; removeNode: (id: Store.nodeId, parent: Store.nodeId) => void; updateNode: (node: any, relationships?: any) => void; - undoOperation: (operation: Operation) => void; + undoOperation: (ops: OT.Op[]) => void; } export const editorStore: StateCreator< @@ -442,8 +441,8 @@ export const editorStore: StateCreator< send(ops); }, - undoOperation: (op: Operation) => { - const inverseOps: OT.Op[] = type.invert(op.data); + undoOperation: (ops: OT.Op[]) => { + const inverseOps: OT.Op[] = type.invert(ops); send(inverseOps); }, }); From 650371375dc1fee54f98969569fb953051800df6 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Wed, 1 May 2024 14:20:37 +0100 Subject: [PATCH 05/10] feat: undo the last N operations to a flow (#3065) --- editor.planx.uk/package.json | 1 + editor.planx.uk/pnpm-lock.yaml | 91 +++++++++- .../src/pages/FlowEditor/index.tsx | 157 ++++++++++++------ 3 files changed, 199 insertions(+), 50 deletions(-) diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index e70d64ea7e..eabf541348 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -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", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index e04ca25390..ed7527aa8f 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -28,6 +28,9 @@ dependencies: '@mui/icons-material': specifier: ^5.15.2 version: 5.15.2(@mui/material@5.15.2)(@types/react@18.2.45)(react@18.2.0) + '@mui/lab': + specifier: 5.0.0-alpha.170 + version: 5.0.0-alpha.170(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.2)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) '@mui/material': specifier: ^5.15.2 version: 5.15.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) @@ -4788,7 +4791,30 @@ packages: '@babel/runtime': 7.24.1 '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.45) - '@mui/utils': 5.15.2(@types/react@18.2.45)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.45)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.45 + clsx: 2.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/base@5.0.0-beta.40(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.45) + '@mui/utils': 5.15.14(@types/react@18.2.45)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.45 clsx: 2.1.0 @@ -4818,6 +4844,39 @@ packages: react: 18.2.0 dev: false + /@mui/lab@5.0.0-alpha.170(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.2)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material': '>=5.15.0' + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.15.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.45)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.45) + '@mui/utils': 5.15.14(@types/react@18.2.45)(react@18.2.0) + '@types/react': 18.2.45 + clsx: 2.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@mui/material@5.15.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JnoIrpNmEHG5uC1IyEdgsnDiaiuCZnUIh7f9oeAr87AvBmNiEJPbo7XrD7kBTFWwp+b97rQ12QdSs9CLhT2n/A==} engines: {node: '>=12.0.0'} @@ -5011,6 +5070,36 @@ packages: react: 18.2.0 dev: false + /@mui/system@5.15.15(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.45)(react@18.2.0): + resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0) + '@mui/private-theming': 5.15.14(@types/react@18.2.45)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.45) + '@mui/utils': 5.15.14(@types/react@18.2.45)(react@18.2.0) + '@types/react': 18.2.45 + clsx: 2.1.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/types@7.2.14(@types/react@18.2.45): resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==} peerDependencies: diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index d46cbc7508..2469932001 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -2,7 +2,13 @@ import "./components/Settings"; import "./floweditor.scss"; import { gql, useSubscription } from "@apollo/client"; -import UndoOutlined from "@mui/icons-material/UndoOutlined"; +import RestoreOutlined from "@mui/icons-material/RestoreOutlined"; +import Timeline from "@mui/lab/Timeline"; +import TimelineConnector from "@mui/lab/TimelineConnector"; +import TimelineContent from "@mui/lab/TimelineContent"; +import TimelineDot from "@mui/lab/TimelineDot"; +import TimelineItem, { timelineItemClasses } from "@mui/lab/TimelineItem"; +import TimelineSeparator from "@mui/lab/TimelineSeparator"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; @@ -10,7 +16,7 @@ import { formatOps } from "@planx/graph"; import { OT } from "@planx/graph/types"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import { formatDistanceToNow } from "date-fns"; -import React, { useRef } from "react"; +import React, { useRef, useState } from "react"; import { rootFlowPath } from "../../routes/utils"; import Flow from "./components/Flow"; @@ -111,6 +117,10 @@ export const LastEdited = () => { }; export const EditHistory = () => { + const [focusedOpIndex, setFocusedOpIndex] = useState( + undefined, + ); + const [flowId, flow, canUserEditTeam, undoOperation] = useStore((state) => [ state.id, state.flow, @@ -152,60 +162,109 @@ export const EditHistory = () => { // Handle missing operations (e.g. non-production data) if (!loading && !data?.operations) return null; + const handleUndo = (i: number) => { + // Get all operations _since_ & including the selected one + const operationsToUndo = data?.operations?.slice(0, i + 1); + + // Make a flattened list, with the latest operations first + const operationsData: Array = []; + operationsToUndo?.map((op) => operationsData.unshift(op?.data)); + const flattenedOperationsData: OT.Op[] = operationsData?.flat(1); + + // Undo all + undoOperation(flattenedOperationsData); + }; + + const inFocus = (i: number): boolean => { + return focusedOpIndex !== undefined && i < focusedOpIndex; + }; + return ( {loading && !data ? ( ) : ( - data?.operations?.map((op: Operation, i: number) => ( - theme.palette.grey[200] }} - > - - - - {`${ - op.actor - ? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}` - : `Created flow` - }`} - - - {formatLastEditDate(op.createdAt)} - - - {i === 0 && ( - undoOperation(op.data)} - disabled={!canUserEditTeam} - color="primary" + + {data?.operations?.map((op: Operation, i: number) => ( + + + + {i < data.operations.length - 1 && ( + + inFocus(i) ? undefined : theme.palette.primary.main, + }} + /> + )} + + + - - - )} - - {op.data && ( - - {[...new Set(formatOps(flow, op.data))].map( - (formattedOp, i) => ( -
  • {formattedOp}
  • - ), + + + {`${ + op.actor + ? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}` + : `Created flow` + }`} + + + {formatLastEditDate(op.createdAt)} + + + {i > 0 && op.actor && ( + handleUndo(i)} + onMouseEnter={() => setFocusedOpIndex(i)} + onMouseLeave={() => setFocusedOpIndex(undefined)} + > + + + )} +
    + {op.data && ( + + {[...new Set(formatOps(flow, op.data))].map( + (formattedOp, i) => ( +
  • {formattedOp}
  • + ), + )} +
    )} -
    - )} -
    - )) + + + ))} + )} ); From adca0b06e2549ced686bcfef4781fd4f03d785a6 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 6 May 2024 10:28:58 +0200 Subject: [PATCH 06/10] add allow list of props for tidier formatted op summaries --- .../shared/Preview/SimpleExpand.tsx | 13 +++-- .../@planx/graph/__tests__/formatOps.test.ts | 22 +++++--- editor.planx.uk/src/@planx/graph/index.ts | 52 ++++++++++++------ .../src/pages/FlowEditor/index.tsx | 54 +++++++++++++++---- 4 files changed, 104 insertions(+), 37 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx index 1726d296cc..0e27b511c9 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx @@ -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%", })); @@ -20,12 +22,14 @@ interface Props { closed: string; }; id: string; + lightFontStyle?: boolean; } const SimpleExpand: React.FC> = ({ children, buttonText, id, + lightFontStyle, }) => { const [show, setShow] = React.useState(false); return ( @@ -35,6 +39,7 @@ const SimpleExpand: React.FC> = ({ onClick={() => setShow(!show)} aria-expanded={show} aria-controls={id} + lightFontStyle={lightFontStyle || false} > {show ? buttonText.closed : buttonText.open} { test("Updating a single property of a node", () => { @@ -118,12 +118,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: "

    Fruits contain seeds and come from the flower of a plant

    ", - }, { p: ["FW5G3EMBI3", "edges"], oi: ["WDwUTbF7Gq", "SO5XbLwSYp", "xTBfSd1Tjy", "zzQAMXexRj"], @@ -141,11 +137,23 @@ describe("Insert operations", () => { ]; expect(formatOps(flowWithChecklist, ops)).toEqual([ - 'Added Checklist description "

    Fruits contain seeds and come from the flower of a plant

    "', "Updated order of Checklist edges", 'Added Answer "Strawberry"', ]); }); + + test("Adding a data property to an existing node that is not in `allowProps`", () => { + const ops = [ + { + p: ["FW5G3EMBI3", "data", "description"], + oi: "

    Fruits contain seeds and come from the flower of a plant

    ", + }, + ]; + + expect(formatOps(flowWithChecklist, ops)).toEqual([ + "Added Checklist description", // only shows "description", not content + ]); + }); }); describe("Remove operations", () => { diff --git a/editor.planx.uk/src/@planx/graph/index.ts b/editor.planx.uk/src/@planx/graph/index.ts index b6dbbb64f7..8c7cf0fbe4 100644 --- a/editor.planx.uk/src/@planx/graph/index.ts +++ b/editor.planx.uk/src/@planx/graph/index.ts @@ -507,6 +507,9 @@ export const sortIdsDepthFirst = export const formatOps = (graph: Graph, ops: Array): string[] => { const output: string[] = []; + // Only show full change description for simple props, omit complex or long ones like `moreInfo` etc + const allowProps = ["title", "text", "fn", "val"]; + // Updating a node or its properties (update = delete + insert) const handleUpdate = (node: Node, op: OT.Object.Replace) => { if (op.od?.type && op.oi?.type) { @@ -516,21 +519,28 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { 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`, @@ -560,11 +570,17 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { }"`, ); } 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")) { const node = graph[op.oi?.[0]]; output.push(`Added ${node?.type ? TYPES[node.type] : "node"} to branch`); @@ -584,11 +600,17 @@ export const formatOps = (graph: Graph, ops: Array): string[] => { }"`, ); } 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")) { const node = graph[op.od?.[0]]; output.push( diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index 5d20e3926b..846c205ca8 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -15,6 +15,7 @@ import IconButton from "@mui/material/IconButton"; import Link from "@mui/material/Link"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; +import SimpleExpand from "@planx/components/shared/Preview/SimpleExpand"; import { formatOps } from "@planx/graph"; import { OT } from "@planx/graph/types"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; @@ -139,6 +140,8 @@ export const LastEdited = () => { }; export const EditHistory = () => { + const OPS_TO_DISPLAY = 5; + const [focusedOpIndex, setFocusedOpIndex] = useState( undefined, ); @@ -278,18 +281,47 @@ export const EditHistory = () => { )} {op.data && ( - - {[...new Set(formatOps(flow, op.data))].map( - (formattedOp, i) => ( -
  • {formattedOp}
  • - ), + <> + + {[...new Set(formatOps(flow, op.data))] + .slice(0, OPS_TO_DISPLAY) + .map((formattedOp, i) => ( +
  • {formattedOp}
  • + ))} +
    + {[...new Set(formatOps(flow, op.data))].length > + OPS_TO_DISPLAY && ( + + + {[...new Set(formatOps(flow, op.data))] + .slice(OPS_TO_DISPLAY) + .map((formattedOp, i) => ( +
  • {formattedOp}
  • + ))} +
    +
    )} -
    + )} From 90c1aff0bdd02fb8216a9dbe2ccb5a7a7ee4c5ec Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 6 May 2024 10:45:30 +0200 Subject: [PATCH 07/10] move EditHistory to own file --- editor.planx.uk/src/@planx/graph/index.ts | 2 +- .../FlowEditor/components/EditHistory.tsx | 214 ++++++++++++++++++ .../FlowEditor/components/PreviewBrowser.tsx | 2 +- .../src/pages/FlowEditor/index.tsx | 209 +---------------- 4 files changed, 219 insertions(+), 208 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx diff --git a/editor.planx.uk/src/@planx/graph/index.ts b/editor.planx.uk/src/@planx/graph/index.ts index 8c7cf0fbe4..cfbe6d5ddf 100644 --- a/editor.planx.uk/src/@planx/graph/index.ts +++ b/editor.planx.uk/src/@planx/graph/index.ts @@ -507,7 +507,7 @@ export const sortIdsDepthFirst = export const formatOps = (graph: Graph, ops: Array): string[] => { const output: string[] = []; - // Only show full change description for simple props, omit complex or long ones like `moreInfo` etc + // Only show full change description for simple props, omit complex or long ones like `moreInfo`, `fileTypes`, etc const allowProps = ["title", "text", "fn", "val"]; // Updating a node or its properties (update = delete + insert) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx new file mode 100644 index 0000000000..91340b5010 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx @@ -0,0 +1,214 @@ +import { gql, useSubscription } from "@apollo/client"; +import RestoreOutlined from "@mui/icons-material/RestoreOutlined"; +import Timeline from "@mui/lab/Timeline"; +import TimelineConnector from "@mui/lab/TimelineConnector"; +import TimelineContent from "@mui/lab/TimelineContent"; +import TimelineDot from "@mui/lab/TimelineDot"; +import TimelineItem, { timelineItemClasses } from "@mui/lab/TimelineItem"; +import TimelineSeparator from "@mui/lab/TimelineSeparator"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import SimpleExpand from "@planx/components/shared/Preview/SimpleExpand"; +import { formatOps } from "@planx/graph"; +import { OT } from "@planx/graph/types"; +import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; +import React, { useState } from "react"; + +import { formatLastEditDate,Operation } from ".."; +import { useStore } from "../lib/store"; + +const EditHistory = () => { + const OPS_TO_DISPLAY = 5; + + const [focusedOpIndex, setFocusedOpIndex] = useState( + undefined, + ); + + const [flowId, flow, canUserEditTeam, teamSlug, undoOperation] = useStore( + (state) => [ + state.id, + state.flow, + state.canUserEditTeam, + state.teamSlug, + state.undoOperation, + ], + ); + + const { data, loading, error } = useSubscription<{ operations: Operation[] }>( + gql` + subscription GetRecentOperations($flow_id: uuid = "") { + operations( + limit: 15 + where: { flow_id: { _eq: $flow_id } } + order_by: { created_at: desc } + ) { + id + createdAt: created_at + actor { + id + firstName: first_name + lastName: last_name + } + data(path: "op") + } + } + `, + { + variables: { + flow_id: flowId, + }, + }, + ); + + if (error) { + console.log(error.message); + return null; + } + + // Handle missing operations (e.g. non-production data) + if (!loading && !data?.operations) return null; + + const handleUndo = (i: number) => { + // Get all operations _since_ & including the selected one + const operationsToUndo = data?.operations?.slice(0, i + 1); + + // Make a flattened list, with the latest operations first + const operationsData: Array = []; + operationsToUndo?.map((op) => operationsData.unshift(op?.data)); + const flattenedOperationsData: OT.Op[] = operationsData?.flat(1); + + // Undo all + undoOperation(flattenedOperationsData); + }; + + const inFocus = (i: number): boolean => { + return focusedOpIndex !== undefined && i < focusedOpIndex; + }; + + return ( + + {loading && !data ? ( + + ) : ( + + {data?.operations?.map((op: Operation, i: number) => ( + + + + inFocus(i) ? undefined : theme.palette.grey[900], + }} + /> + {i < data.operations.length - 1 && ( + + inFocus(i) ? undefined : theme.palette.grey[900], + }} + /> + )} + + + + + + {`${ + op.actor + ? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}` + : `Created flow` + }`} + + + {formatLastEditDate(op.createdAt)} + + + {i > 0 && op.actor && canUserEditTeam(teamSlug) && ( + handleUndo(i)} + onMouseEnter={() => setFocusedOpIndex(i)} + onMouseLeave={() => setFocusedOpIndex(undefined)} + > + + + )} + + {op.data && ( + <> + + {[...new Set(formatOps(flow, op.data))] + .slice(0, OPS_TO_DISPLAY) + .map((formattedOp, i) => ( +
  • {formattedOp}
  • + ))} +
    + {[...new Set(formatOps(flow, op.data))].length > + OPS_TO_DISPLAY && ( + + + {[...new Set(formatOps(flow, op.data))] + .slice(OPS_TO_DISPLAY) + .map((formattedOp, i) => ( +
  • {formattedOp}
  • + ))} +
    +
    + )} + + )} +
    +
    + ))} +
    + )} +
    + ); +}; + +export default EditHistory; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx b/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx index 2b94126ccb..7d5b76f93a 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx @@ -34,8 +34,8 @@ import Caret from "ui/icons/Caret"; import Input from "ui/shared/Input"; import Questions from "../../Preview/Questions"; -import { EditHistory } from ".."; import { useStore } from "../lib/store"; +import EditHistory from "./EditHistory"; const Console = styled(Box)(() => ({ overflow: "auto", diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index 846c205ca8..e5cf8005a8 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -3,24 +3,13 @@ import "./floweditor.scss"; import { gql, useSubscription } from "@apollo/client"; import EditNoteIcon from "@mui/icons-material/EditNote"; -import RestoreOutlined from "@mui/icons-material/RestoreOutlined"; -import Timeline from "@mui/lab/Timeline"; -import TimelineConnector from "@mui/lab/TimelineConnector"; -import TimelineContent from "@mui/lab/TimelineContent"; -import TimelineDot from "@mui/lab/TimelineDot"; -import TimelineItem, { timelineItemClasses } from "@mui/lab/TimelineItem"; -import TimelineSeparator from "@mui/lab/TimelineSeparator"; import Box from "@mui/material/Box"; -import IconButton from "@mui/material/IconButton"; import Link from "@mui/material/Link"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import SimpleExpand from "@planx/components/shared/Preview/SimpleExpand"; -import { formatOps } from "@planx/graph"; import { OT } from "@planx/graph/types"; -import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import { formatDistanceToNow } from "date-fns"; -import React, { useRef, useState } from "react"; +import React, { useRef } from "react"; import { rootFlowPath } from "../../routes/utils"; import Flow from "./components/Flow"; @@ -39,12 +28,13 @@ export interface Operation { data: Array; } -const formatLastEditDate = (date: string): string => { +export const formatLastEditDate = (date: string): string => { return formatDistanceToNow(new Date(date), { includeSeconds: true, addSuffix: true, }); }; + const EditorContainer = styled(Box)(() => ({ display: "flex", alignItems: "stretch", @@ -139,199 +129,6 @@ export const LastEdited = () => { ); }; -export const EditHistory = () => { - const OPS_TO_DISPLAY = 5; - - const [focusedOpIndex, setFocusedOpIndex] = useState( - undefined, - ); - - const [flowId, flow, canUserEditTeam, teamSlug, undoOperation] = useStore( - (state) => [ - state.id, - state.flow, - state.canUserEditTeam, - state.teamSlug, - state.undoOperation, - ], - ); - - const { data, loading, error } = useSubscription<{ operations: Operation[] }>( - gql` - subscription GetRecentOperations($flow_id: uuid = "") { - operations( - limit: 15 - where: { flow_id: { _eq: $flow_id } } - order_by: { created_at: desc } - ) { - id - createdAt: created_at - actor { - id - firstName: first_name - lastName: last_name - } - data(path: "op") - } - } - `, - { - variables: { - flow_id: flowId, - }, - }, - ); - - if (error) { - console.log(error.message); - return null; - } - - // Handle missing operations (e.g. non-production data) - if (!loading && !data?.operations) return null; - - const handleUndo = (i: number) => { - // Get all operations _since_ & including the selected one - const operationsToUndo = data?.operations?.slice(0, i + 1); - - // Make a flattened list, with the latest operations first - const operationsData: Array = []; - operationsToUndo?.map((op) => operationsData.unshift(op?.data)); - const flattenedOperationsData: OT.Op[] = operationsData?.flat(1); - - // Undo all - undoOperation(flattenedOperationsData); - }; - - const inFocus = (i: number): boolean => { - return focusedOpIndex !== undefined && i < focusedOpIndex; - }; - - return ( - - {loading && !data ? ( - - ) : ( - - {data?.operations?.map((op: Operation, i: number) => ( - - - - inFocus(i) ? undefined : theme.palette.grey[900], - }} - /> - {i < data.operations.length - 1 && ( - - inFocus(i) ? undefined : theme.palette.grey[900], - }} - /> - )} - - - - - - {`${ - op.actor - ? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}` - : `Created flow` - }`} - - - {formatLastEditDate(op.createdAt)} - - - {i > 0 && op.actor && canUserEditTeam(teamSlug) && ( - handleUndo(i)} - onMouseEnter={() => setFocusedOpIndex(i)} - onMouseLeave={() => setFocusedOpIndex(undefined)} - > - - - )} - - {op.data && ( - <> - - {[...new Set(formatOps(flow, op.data))] - .slice(0, OPS_TO_DISPLAY) - .map((formattedOp, i) => ( -
  • {formattedOp}
  • - ))} -
    - {[...new Set(formatOps(flow, op.data))].length > - OPS_TO_DISPLAY && ( - - - {[...new Set(formatOps(flow, op.data))] - .slice(OPS_TO_DISPLAY) - .map((formattedOp, i) => ( -
  • {formattedOp}
  • - ))} -
    -
    - )} - - )} -
    -
    - ))} -
    - )} -
    - ); -}; - const FlowEditor: React.FC = ({ flow, breadcrumbs }) => { const scrollContainerRef = useRef(null); useScrollControlsAndRememberPosition(scrollContainerRef); From c737103ec8763a50211332921bf792e070c7cd6f Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 6 May 2024 11:26:46 +0200 Subject: [PATCH 08/10] better formatOps tests --- .../@planx/graph/__tests__/formatOps.test.ts | 33 +++++++++++++++++-- .../FlowEditor/components/EditHistory.tsx | 31 ++++++++++------- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/editor.planx.uk/src/@planx/graph/__tests__/formatOps.test.ts b/editor.planx.uk/src/@planx/graph/__tests__/formatOps.test.ts index 05751a6ac7..5d66948318 100644 --- a/editor.planx.uk/src/@planx/graph/__tests__/formatOps.test.ts +++ b/editor.planx.uk/src/@planx/graph/__tests__/formatOps.test.ts @@ -1,7 +1,7 @@ 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"], @@ -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 ]); }); @@ -142,6 +156,19 @@ describe("Insert operations", () => { ]); }); + 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 = [ { @@ -151,7 +178,7 @@ describe("Insert operations", () => { ]; expect(formatOps(flowWithChecklist, ops)).toEqual([ - "Added Checklist description", // only shows "description", not content + "Added Checklist description", // only shows prop name "description", not content ]); }); }); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx index 91340b5010..33dc1fc68d 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx @@ -15,7 +15,7 @@ import { OT } from "@planx/graph/types"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import React, { useState } from "react"; -import { formatLastEditDate,Operation } from ".."; +import { formatLastEditDate, Operation } from ".."; import { useStore } from "../lib/store"; const EditHistory = () => { @@ -82,7 +82,8 @@ const EditHistory = () => { undoOperation(flattenedOperationsData); }; - const inFocus = (i: number): boolean => { + const inUndoScope = (i: number): boolean => { + // Is a given operation in the list in scope of also being "undone" if the currently focused button is clicked? return focusedOpIndex !== undefined && i < focusedOpIndex; }; @@ -105,14 +106,18 @@ const EditHistory = () => { - inFocus(i) ? undefined : theme.palette.grey[900], + inUndoScope(i) + ? theme.palette.grey[400] + : theme.palette.grey[900], }} /> {i < data.operations.length - 1 && ( - inFocus(i) ? undefined : theme.palette.grey[900], + inUndoScope(i) + ? theme.palette.grey[400] + : theme.palette.grey[900], }} /> )} @@ -129,7 +134,7 @@ const EditHistory = () => { {`${ op.actor @@ -139,7 +144,7 @@ const EditHistory = () => { {formatLastEditDate(op.createdAt)} @@ -154,7 +159,7 @@ const EditHistory = () => { > )} @@ -165,12 +170,14 @@ const EditHistory = () => { variant="body2" component="ul" padding={2} - color={inFocus(i) ? "GrayText" : "inherit"} + color={inUndoScope(i) ? "GrayText" : "inherit"} > {[...new Set(formatOps(flow, op.data))] .slice(0, OPS_TO_DISPLAY) .map((formattedOp, i) => ( -
  • {formattedOp}
  • +
  • + {formattedOp} +
  • ))} {[...new Set(formatOps(flow, op.data))].length > @@ -190,12 +197,14 @@ const EditHistory = () => { variant="body2" component="ul" padding={2} - color={inFocus(i) ? "GrayText" : "inherit"} + color={inUndoScope(i) ? "GrayText" : "inherit"} > {[...new Set(formatOps(flow, op.data))] .slice(OPS_TO_DISPLAY) .map((formattedOp, i) => ( -
  • {formattedOp}
  • +
  • + {formattedOp} +
  • ))} From f86d0c990ea97d8f921b66f7cd62595c26cde3c7 Mon Sep 17 00:00:00 2001 From: Ian Jones <51156018+ianjon3s@users.noreply.github.com> Date: Tue, 7 May 2024 17:05:04 +0100 Subject: [PATCH 09/10] feat: Layout for undo history (#3116) --- .../FlowEditor/components/EditHistory.tsx | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx index 33dc1fc68d..9444a87c5c 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/EditHistory.tsx @@ -8,16 +8,36 @@ import TimelineItem, { timelineItemClasses } from "@mui/lab/TimelineItem"; import TimelineSeparator from "@mui/lab/TimelineSeparator"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; +import { styled } from "@mui/material/styles"; +import Tooltip, { tooltipClasses, TooltipProps } from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import SimpleExpand from "@planx/components/shared/Preview/SimpleExpand"; import { formatOps } from "@planx/graph"; import { OT } from "@planx/graph/types"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import React, { useState } from "react"; +import { FONT_WEIGHT_SEMI_BOLD } from "theme"; import { formatLastEditDate, Operation } from ".."; import { useStore } from "../lib/store"; +const TooltipWrap = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.background.dark, + fontSize: "0.8em", + borderRadius: 0, + fontWeight: FONT_WEIGHT_SEMI_BOLD, + }, +})); + +const HistoryListItem = styled("li")(() => ({ + listStyleType: "square", + overflowWrap: "break-word", + wordWrap: "break-word", +})); + const EditHistory = () => { const OPS_TO_DISPLAY = 5; @@ -94,6 +114,7 @@ const EditHistory = () => { ) : ( { /> )} - + {`${ @@ -150,18 +178,19 @@ const EditHistory = () => { {i > 0 && op.actor && canUserEditTeam(teamSlug) && ( - handleUndo(i)} - onMouseEnter={() => setFocusedOpIndex(i)} - onMouseLeave={() => setFocusedOpIndex(undefined)} - > - - + + handleUndo(i)} + onMouseEnter={() => setFocusedOpIndex(i)} + onMouseLeave={() => setFocusedOpIndex(undefined)} + > + + + )} {op.data && ( @@ -171,13 +200,14 @@ const EditHistory = () => { component="ul" padding={2} color={inUndoScope(i) ? "GrayText" : "inherit"} + style={{ paddingRight: "50px" }} > {[...new Set(formatOps(flow, op.data))] .slice(0, OPS_TO_DISPLAY) .map((formattedOp, i) => ( -
  • + {formattedOp} -
  • + ))} {[...new Set(formatOps(flow, op.data))].length > @@ -198,13 +228,14 @@ const EditHistory = () => { component="ul" padding={2} color={inUndoScope(i) ? "GrayText" : "inherit"} + style={{ paddingRight: "50px" }} > {[...new Set(formatOps(flow, op.data))] .slice(OPS_TO_DISPLAY) .map((formattedOp, i) => ( -
  • + {formattedOp} -
  • + ))} From 53009bf83143f571f4f0ea4252b3ba1528bf0625 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Wed, 8 May 2024 12:48:59 +0200 Subject: [PATCH 10/10] remove featureFlag for PO testing --- editor.planx.uk/src/lib/featureFlags.ts | 2 +- .../pages/FlowEditor/components/PreviewBrowser.tsx | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts index 85fc186aa8..022965a276 100644 --- a/editor.planx.uk/src/lib/featureFlags.ts +++ b/editor.planx.uk/src/lib/featureFlags.ts @@ -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]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx b/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx index 7d5b76f93a..f3f0905dce 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx @@ -26,10 +26,8 @@ import Typography from "@mui/material/Typography"; import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import { AxiosError } from "axios"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; -import { hasFeatureFlag } from "lib/featureFlags"; import React, { useState } from "react"; import { useAsync } from "react-use"; -import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; import Caret from "ui/icons/Caret"; import Input from "ui/shared/Input"; @@ -394,9 +392,7 @@ const PreviewBrowser: React.FC<{ const [alteredNodes, setAlteredNodes] = useState(); const [dialogOpen, setDialogOpen] = useState(false); const [summary, setSummary] = useState(); - const [activeTab, setActiveTab] = useState( - hasFeatureFlag("UNDO") ? "History" : "PreviewBrowser", - ); // temp hack to keep History panel in view while editing/re-rendering + const [activeTab, setActiveTab] = useState("PreviewBrowser"); const handleChange = (event: React.SyntheticEvent, newValue: SideBarTabs) => { setActiveTab(newValue); @@ -656,11 +652,7 @@ const PreviewBrowser: React.FC<{ {activeTab === "History" && ( - {hasFeatureFlag("UNDO") ? ( - - ) : ( - - )} + )}