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 1 commit
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
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);
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved

// 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`]: {
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
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
Loading