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}
  • + ), + )} +
    )} - - )} -
    - )) + + + ))} + )} );