feat: introduce automatic layout for new flow editor
This commit is contained in:

committed by
Ali BARIN

parent
bac4ab5aa4
commit
d6abf283bc
@@ -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';
|
||||
@@ -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"
|
||||
|
@@ -1,12 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { FlowPropType } from 'propTypes/propTypes';
|
||||
|
||||
function EditorNew(props) {
|
||||
return <div>new editor comes here</div>;
|
||||
}
|
||||
|
||||
EditorNew.propTypes = {
|
||||
flow: FlowPropType.isRequired,
|
||||
};
|
||||
|
||||
export default EditorNew;
|
148
packages/web/src/components/EditorNew/EditorNew.jsx
Normal file
148
packages/web/src/components/EditorNew/EditorNew.jsx
Normal file
@@ -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
packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx
Normal file
37
packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx
Normal file
@@ -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
packages/web/src/components/EditorNew/useAutoLayout.js
Normal file
68
packages/web/src/components/EditorNew/useAutoLayout.js
Normal file
@@ -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]);
|
||||
};
|
Reference in New Issue
Block a user