Skip to content

Commit

Permalink
feat: undo the last N operations to a flow (#3065)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamcinchak authored May 1, 2024
1 parent 1a6d80b commit 6503713
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 50 deletions.
1 change: 1 addition & 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
91 changes: 90 additions & 1 deletion editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 108 additions & 49 deletions editor.planx.uk/src/pages/FlowEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ 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";
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";
Expand Down Expand Up @@ -111,6 +117,10 @@ export const LastEdited = () => {
};

export const EditHistory = () => {
const [focusedOpIndex, setFocusedOpIndex] = useState<number | undefined>(
undefined,
);

const [flowId, flow, canUserEditTeam, undoOperation] = useStore((state) => [
state.id,
state.flow,
Expand Down Expand Up @@ -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<OT.Op[]> = [];
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 (
<Box>
{loading && !data ? (
<DelayedLoadingIndicator />
) : (
data?.operations?.map((op: Operation, i: number) => (
<Box
key={`container-${op.id}`}
marginBottom={2}
padding={2}
sx={{ background: (theme) => theme.palette.grey[200] }}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Box>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{`${
op.actor
? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}`
: `Created flow`
}`}
</Typography>
<Typography variant="body2">
{formatLastEditDate(op.createdAt)}
</Typography>
</Box>
{i === 0 && (
<IconButton
title="Undo"
aria-label="Undo"
onClick={() => undoOperation(op.data)}
disabled={!canUserEditTeam}
color="primary"
<Timeline
sx={{
[`& .${timelineItemClasses.root}:before`]: {
flex: 0,
padding: 0,
},
}}
>
{data?.operations?.map((op: Operation, i: number) => (
<TimelineItem key={op.id}>
<TimelineSeparator>
<TimelineDot color={inFocus(i) ? undefined : "primary"} />
{i < data.operations.length - 1 && (
<TimelineConnector
sx={{
bgcolor: (theme) =>
inFocus(i) ? undefined : theme.palette.primary.main,
}}
/>
)}
</TimelineSeparator>
<TimelineContent>
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<UndoOutlined />
</IconButton>
)}
</Box>
{op.data && (
<Typography variant="body2" component="ul" padding={2}>
{[...new Set(formatOps(flow, op.data))].map(
(formattedOp, i) => (
<li key={i}>{formattedOp}</li>
),
<Box>
<Typography
variant="body1"
sx={{ fontWeight: 600 }}
color={inFocus(i) ? "GrayText" : "inherit"}
>
{`${
op.actor
? `Edited by ${op.actor?.firstName} ${op.actor?.lastName}`
: `Created flow`
}`}
</Typography>
<Typography
variant="body2"
color={inFocus(i) ? "GrayText" : "inherit"}
>
{formatLastEditDate(op.createdAt)}
</Typography>
</Box>
{i > 0 && op.actor && (
<IconButton
title="Restore to this point"
aria-label="Restore to this point"
onClick={() => handleUndo(i)}
onMouseEnter={() => setFocusedOpIndex(i)}
onMouseLeave={() => setFocusedOpIndex(undefined)}
>
<RestoreOutlined
fontSize="large"
color={inFocus(i) ? "inherit" : "primary"}
/>
</IconButton>
)}
</Box>
{op.data && (
<Typography
variant="body2"
component="ul"
padding={2}
color={inFocus(i) ? "GrayText" : "inherit"}
>
{[...new Set(formatOps(flow, op.data))].map(
(formattedOp, i) => (
<li key={i}>{formattedOp}</li>
),
)}
</Typography>
)}
</Typography>
)}
</Box>
))
</TimelineContent>
</TimelineItem>
))}
</Timeline>
)}
</Box>
);
Expand Down

0 comments on commit 6503713

Please sign in to comment.