Skip to content

Commit

Permalink
feat: introduce automatic layout for new flow editor
Browse files Browse the repository at this point in the history
  • Loading branch information
kattoczko committed May 10, 2024
1 parent db2940f commit a6a6f60
Show file tree
Hide file tree
Showing 8 changed files with 670 additions and 25 deletions.
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@apollo/client": "^3.6.9",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.8",
Expand All @@ -32,6 +33,7 @@
"react-router-dom": "^6.0.2",
"react-scripts": "5.0.0",
"react-window": "^1.8.9",
"reactflow": "^11.11.2",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.94.2",
Expand Down
35 changes: 22 additions & 13 deletions packages/web/src/components/EditorLayout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import Snackbar from '@mui/material/Snackbar';
import { ReactFlowProvider } from 'reactflow';

import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography';
Expand Down Expand Up @@ -134,20 +135,28 @@ export default function EditorLayout() {
</Button>
</Box>
</TopBar>
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}

{flow &&
(useNewFlowEditor ? (
<EditorNew flow={flow} />
) : (
<Editor flow={flow} />
))}
</EditorProvider>
</Container>
</Stack>
{useNewFlowEditor ? (
<Stack direction="column" height="100%" flexGrow={1}>
<Stack direction="column" flexGrow={1}>
<EditorProvider value={{ readOnly: !!flow?.active }}>
<ReactFlowProvider>
{!flow && !isFlowLoading && 'not found'}
{flow && <EditorNew flow={flow} />}
</ReactFlowProvider>
</EditorProvider>
</Stack>
</Stack>
) : (
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
)}

<Snackbar
data-test="flow-cannot-edit-info-snackbar"
Expand Down
12 changes: 0 additions & 12 deletions packages/web/src/components/EditorNew/EditorNew.js

This file was deleted.

148 changes: 148 additions & 0 deletions packages/web/src/components/EditorNew/EditorNew.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useEffect, useState, useCallback } from 'react';
import { useMutation } from '@apollo/client';
import { useQueryClient } from '@tanstack/react-query';
import { FlowPropType } from 'propTypes/propTypes';
import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
import { Stack } from '@mui/material';

import { UPDATE_STEP } from 'graphql/mutations/update-step';
import FlowStep from './FlowStep/FlowStep';
import { useAutoLayout } from './useAutoLayout';

const nodeTypes = { flowStep: FlowStep };

const EditorNew = ({ flow }) => {
const [triggerStep] = flow.steps;
const [currentStepId, setCurrentStepId] = useState(triggerStep.id);

const [updateStep] = useMutation(UPDATE_STEP);
const queryClient = useQueryClient();

const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
useAutoLayout();

const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges],
);

const openNextStep = useCallback(
(nextStep) => () => {
setCurrentStepId(nextStep?.id);
},
[],
);

const onStepChange = useCallback(
async (step) => {
const mutationInput = {
id: step.id,
key: step.key,
parameters: step.parameters,
connection: {
id: step.connection?.id,
},
flow: {
id: flow.id,
},
};

if (step.appKey) {
mutationInput.appKey = step.appKey;
}

await updateStep({
variables: { input: mutationInput },
});
await queryClient.invalidateQueries({
queryKey: ['steps', step.id, 'connection'],
});
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
},
[flow.id, updateStep, queryClient],
);

useEffect(() => {
setNodes(
nodes.map((node) => ({
...node,
data: { ...node.data, collapsed: currentStepId !== node.data.step.id },
})),
);
}, [currentStepId]);

useEffect(() => {
const getInitialNodes = () => {
return flow?.steps?.map((step, index) => ({
id: step.id,
position: { x: 0, y: 0 },
data: {
step,
index: index,
flowId: flow.id,
collapsed: currentStepId !== step.id,
openNextStep: openNextStep(flow?.steps[index + 1]),
onOpen: () => setCurrentStepId(step.id),
onClose: () => setCurrentStepId(null),
onChange: onStepChange,
},
type: 'flowStep',
}));
};

const getInitialEdges = () => {
return flow?.steps?.map((step, i) => {
const sourceId = step.id;
const targetId = flow.steps[i + 1]?.id;
return {
id: i,
source: sourceId,
target: targetId,
animated: false,
};
});
};

const nodes = getInitialNodes();
const edges = getInitialEdges();

setNodes(nodes);
setEdges(edges);
}, []);

return (
<Stack
direction="column"
sx={{
flexGrow: 1,
'& > div': {
flexGrow: 1,
},
}}
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
panOnScroll
panOnScrollMode="vertical"
panOnDrag={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
panActivationKeyCode={null}
></ReactFlow>
</Stack>
);
};

EditorNew.propTypes = {
flow: FlowPropType.isRequired,
};

export default EditorNew;
37 changes: 37 additions & 0 deletions packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Handle, Position } from 'reactflow';
import FlowStepBase from 'components/FlowStep';
import { Box } from '@mui/material';

function FlowStep({
data: {
step,
index,
flowId,
collapsed,
openNextStep,
onOpen,
onClose,
onChange,
currentStepId,
},
selected,
}) {
return (
<Box maxWidth={900} width="100vw" className="nodrag">
<Handle type="target" position={Position.Top} />
<FlowStepBase
step={step}
index={index + 1}
collapsed={collapsed}
onOpen={onOpen}
onClose={onClose}
onChange={onChange}
flowId={flowId}
onContinue={openNextStep}
/>
<Handle type="source" position={Position.Bottom} />
</Box>
);
}

export default FlowStep;
68 changes: 68 additions & 0 deletions packages/web/src/components/EditorNew/useAutoLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useEffect, useMemo } from 'react';
import Dagre from '@dagrejs/dagre';
import { usePrevious } from 'hooks/usePrevious';
import { isEqual } from 'lodash';
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';

export const useAutoLayout = () => {
const nodes = useNodes();
const prevNodes = usePrevious(nodes);
const nodesInitialized = useNodesInitialized();
const { getEdges, setNodes, setEdges } = useReactFlow();

const graph = useMemo(
() => new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})),
[],
);

const getLayoutedElements = useCallback(
(nodes, edges) => {
graph.setGraph({
rankdir: 'TB',
marginy: 60,
marginx: 60,
universalSep: true,
});

edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
nodes.forEach((node) => graph.setNode(node.id, node));

Dagre.layout(graph);

return {
nodes: nodes.map((node) => {
const { x, y, width, height } = graph.node(node.id);
return {
...node,
position: { x: x - width / 2, y: y - height / 2 },
};
}),
edges,
};
},
[graph],
);

const onLayout = useCallback(
(nodes, edges) => {
const layouted = getLayoutedElements(nodes, edges);

setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
},
[setEdges, setNodes, getLayoutedElements],
);

useEffect(() => {
const shouldAutoLayout =
nodesInitialized &&
!isEqual(
nodes.map(({ width, height }) => ({ width, height })),
prevNodes.map(({ width, height }) => ({ width, height })),
);

if (shouldAutoLayout) {
onLayout(nodes, getEdges());
}
}, [nodes]);
};
9 changes: 9 additions & 0 deletions packages/web/src/hooks/usePrevious.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useEffect, useRef } from "react";

export const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
Loading

0 comments on commit a6a6f60

Please sign in to comment.