From 3e07ab8b1da77883f2a8d61d684531d14f750bad Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 8 Jun 2024 09:46:53 -0500 Subject: [PATCH 01/12] Use reactflow to render project workflow --- package.json | 2 + razzle.config.js | 5 +- src/common/transitionTypeStyles.tsx | 4 +- src/scenes/Projects/Projects.tsx | 8 + .../Flowchart/ProjectFlowchart.graphql | 5 + .../Workflow/Flowchart/ProjectFlowchart.tsx | 90 +++ .../Projects/Workflow/Flowchart/layout.ts | 23 + .../Projects/Workflow/Flowchart/nodes.tsx | 126 +++++ .../Workflow/Flowchart/parse-node-edges.tsx | 109 ++++ .../Workflow/Flowchart/workflow.graphql | 45 ++ yarn.lock | 521 +++++++++++++++++- 11 files changed, 934 insertions(+), 4 deletions(-) create mode 100644 src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql create mode 100644 src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx create mode 100644 src/scenes/Projects/Workflow/Flowchart/layout.ts create mode 100644 src/scenes/Projects/Workflow/Flowchart/nodes.tsx create mode 100644 src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx create mode 100644 src/scenes/Projects/Workflow/Flowchart/workflow.graphql diff --git a/package.json b/package.json index 9af9ce7e4..701d23020 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@apollo/client": "3.7.*", "@babel/runtime": "^7.23.2", + "@dagrejs/dagre": "^1.1.2", "@date-io/core": "^2.17.0", "@date-io/luxon": "^2.17.0", "@editorjs/checklist": "^1.5.0", @@ -105,6 +106,7 @@ "react-pdf": "^5.7.2", "react-router": "^6.17.0", "react-router-dom": "^6.17.0", + "reactflow": "^11.11.3", "response-time": "^2.3.2", "rifm": "^0.12.1", "serialize-query-params": "^1.3.6", diff --git a/razzle.config.js b/razzle.config.js index c85c5f21f..1f1dc1609 100644 --- a/razzle.config.js +++ b/razzle.config.js @@ -23,7 +23,10 @@ const modifyWebpackOptions = ({ options.babelRule.include.push( require.resolve('@seedcompany/common').replace('.cjs', '.js'), require.resolve('@editorjs/editorjs').replace('.umd.js', '.mjs'), - (path) => path.includes('/@mui-') + (path) => + path.includes('/@mui-') || + path.includes('reactflow') || + path.includes('dagrejs') ); return options; diff --git a/src/common/transitionTypeStyles.tsx b/src/common/transitionTypeStyles.tsx index 89c74f4ce..76f28f9c3 100644 --- a/src/common/transitionTypeStyles.tsx +++ b/src/common/transitionTypeStyles.tsx @@ -1,8 +1,8 @@ import { ButtonProps } from '@mui/material'; import { TransitionType } from '~/api/schema/schema.graphql'; -export const transitionTypeStyles: Record = { +export const transitionTypeStyles = { Approve: { color: 'primary', variant: 'contained' }, Neutral: { color: 'secondary', variant: 'text' }, Reject: { color: 'error', variant: 'text' }, -}; +} satisfies Record; diff --git a/src/scenes/Projects/Projects.tsx b/src/scenes/Projects/Projects.tsx index 8cfbb17bd..0b3506c5d 100644 --- a/src/scenes/Projects/Projects.tsx +++ b/src/scenes/Projects/Projects.tsx @@ -38,9 +38,17 @@ const ChangeRequestList = loadable(() => import('./ChangeRequest/List'), { resolveComponent: (m) => m.ProjectChangeRequestList, }); +const ProjectFlowchart = loadable( + () => import('./Workflow/Flowchart/ProjectFlowchart'), + { + resolveComponent: (m) => m.default, + } +); + export const Projects = () => ( } /> + } /> } /> {NotFoundRoute} diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql new file mode 100644 index 000000000..41ba86237 --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql @@ -0,0 +1,5 @@ +query ProjectFlowchart { + workflow: projectWorkflow { + ...workflow + } +} diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx new file mode 100644 index 000000000..227ad339f --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx @@ -0,0 +1,90 @@ +import { useQuery } from '@apollo/client'; +import { ComponentType, useEffect } from 'react'; +import ReactFlow, { + Background, + Controls, + EdgeTypes, + NodeProps, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, +} from 'reactflow'; +import { determinePositions } from './layout'; +import { + Edge as EdgeComponent, + FlowchartStyles, + StateNode, + TransitionNode, +} from './nodes'; +import { NodeTypes, parseWorkflow } from './parse-node-edges'; +import { ProjectFlowchartDocument } from './ProjectFlowchart.graphql'; + +const WrappedFlowchart = () => ( + + + +); +// eslint-disable-next-line react/display-name,import/no-default-export +export default WrappedFlowchart; + +const ProjectFlowchart = () => { + const { fitView } = useReactFlow(); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const { data } = useQuery(ProjectFlowchartDocument); + useEffect(() => { + const workflow = data?.workflow; + if (!workflow) { + return; + } + const { nodes, edges } = parseWorkflow(workflow); + + const positionedNodes = determinePositions(nodes, edges); + setNodes([...positionedNodes]); + setEdges([...edges]); + window.requestAnimationFrame(() => { + fitView(); + }); + + // TODO Better: render, to get size, then do layout + setTimeout(() => { + setNodes((prev) => { + const positionedNodes = determinePositions(prev, edges); + setTimeout(() => { + fitView(); + }, 100); + return positionedNodes; + }); + }, 100); + }, [data, fitView, setEdges, setNodes]); + + return ( + + + + + + + ); +}; + +export const nodeTypes: Record> = { + state: StateNode, + transition: TransitionNode, +}; + +const edgeTypes = { + default: EdgeComponent, +} satisfies EdgeTypes; diff --git a/src/scenes/Projects/Workflow/Flowchart/layout.ts b/src/scenes/Projects/Workflow/Flowchart/layout.ts new file mode 100644 index 000000000..de151592d --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/layout.ts @@ -0,0 +1,23 @@ +import Dagre from '@dagrejs/dagre'; +import { Edge, Node } from 'reactflow'; + +const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + +export const determinePositions = (nodes: Node[], edges: Edge[]) => { + g.setGraph({ rankdir: 'TB' }); + + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + nodes.forEach((node) => g.setNode(node.id, node as any)); + + Dagre.layout(g); + + return nodes.map((node) => { + const position = g.node(node.id); + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + const x = position.x - (node.width ?? 0) / 2; + const y = position.y - (node.height ?? 0) / 2; + + return { ...node, position: { x, y } }; + }); +}; diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx new file mode 100644 index 000000000..5c415302f --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx @@ -0,0 +1,126 @@ +import { Box, Card, CardProps } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { BezierEdge, EdgeProps, Handle, NodeProps, Position } from 'reactflow'; +import { extendSx } from '~/common'; +import { transitionTypeStyles } from '~/common/transitionTypeStyles'; +import { isBack } from './parse-node-edges'; +import { + WorkflowStateFragment as State, + WorkflowTransitionFragment as Transition, +} from './workflow.graphql'; + +import 'reactflow/dist/style.css'; + +export const FlowchartStyles = styled(Box)(({ theme }) => ({ + height: '100%', + '.react-flow__attribution': { display: 'none' }, + '& .react-flow': { + '.react-flow__edge-textbg': { + fill: theme.palette.background.paper, + }, + '.react-flow__handle': { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + transform: 'translate(-50%, -50%)', + background: 'transparent', + border: 'none', + }, + '.react-flow__edge': { + '.react-flow__edge-path, .react-flow__connection-path': { + stroke: 'var(--color)', + strokeWidth: 2, + }, + '&.selected, &:focus, &:focus-visible': { + '.react-flow__edge-path': { + strokeWidth: 3, + }, + }, + }, + }, +})); + +export function StateNode({ data, selected }: NodeProps) { + return ( + <> + + + {data.label} + + + + + ); +} + +export function TransitionNode({ data, selected }: NodeProps) { + const { color } = transitionTypeStyles[data.type]; + const back = isBack(data); + return ( + <> + + + {data.label} + + + + ); +} + +const NodeCard = ({ + children, + selected, + color, + sx, +}: CardProps & Pick) => ( + ({ + transition: theme.transitions.create(['box-shadow', 'border-color'], { + duration: theme.transitions.duration.shorter, + }), + borderColor: selected ? `${color}.dark` : 'transparent', + borderWidth: 1, + borderStyle: 'solid', + p: 2, + bgcolor: `${color}.main`, + color: `${color}.contrastText`, + }), + ...extendSx(sx), + ]} + > + {children} + +); + +export const Edge = (props: EdgeProps) => { + const { color } = transitionTypeStyles[props.data!.type]; + return ( + ({ '--color': theme.palette[color].light })} + > + + + ); +}; diff --git a/src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx b/src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx new file mode 100644 index 000000000..36e94d7c9 --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx @@ -0,0 +1,109 @@ +import { cmpBy } from '@seedcompany/common'; +import { uniqBy } from 'lodash'; +import { Fragment } from 'react'; +import { Edge, Node } from 'reactflow'; +import { LiteralUnion } from 'type-fest'; +import { WorkflowTransitionDynamicTo } from '~/api/schema/schema.graphql'; +import { isTypename } from '~/common'; +import { + WorkflowStateFragment as State, + WorkflowTransitionFragment as Transition, + WorkflowFragment as Workflow, +} from './workflow.graphql'; + +export type NodeTypes = LiteralUnion<'state' | 'transition', string>; + +export function parseWorkflow(workflow: Workflow) { + const states = workflow.states.map( + (state): Node => ({ + id: state.value, + type: 'state', + data: state, + position: { x: 0, y: 0 }, + }) + ); + + const transitionEnds = uniqBy( + workflow.transitions.toSorted( + cmpBy((t) => { + const endState = + t.to.__typename === 'WorkflowTransitionStaticTo' + ? t.to.state.value + : t.to.relatedStates[0]!.value; + return workflow.states.findIndex((e) => e.value === endState); + }) + ), + transitionEndId + ); + const transitionEndNodes = transitionEnds.map( + (t): Node => ({ + id: transitionEndId(t), + type: 'transition', + data: t, + position: { x: 0, y: 0 }, + }) + ); + const nodes = [...states, ...transitionEndNodes].reverse(); + + const edges = uniqBy( + workflow.transitions + .flatMap>((t) => [ + ...t.from.map((from) => ({ + id: `${from.value} -> ${transitionEndId(t)}`, + source: from.value, + target: transitionEndId(t), + targetHandle: 'forward', + label: ( + <> + {t.conditions.map((c) => ( + + {c.label} +
+
+ ))} + + ), + data: t, + })), + ...(isDynamic(t.to) + ? t.to.relatedStates.map((state) => ({ + id: `${transitionEndId(t)} -> ${state.value}`, + source: transitionEndId(t), + target: state.value, + targetHandle: isBack(t) ? 'back' : 'forward', + label: isBack(t) ? 'Back' : undefined, + data: t, + })) + : [ + { + id: `${transitionEndId(t)} -> ${t.to.state.value}`, + source: transitionEndId(t), + target: t.to.state.value, + targetHandle: 'forward', + data: t, + }, + ]), + ]) + .map((e) => ({ + ...e, + animated: e.animated ?? true, + })), + (e) => e.id + ); + + return { nodes, edges }; +} + +export const isBack = (t: Transition) => + isDynamic(t.to) && t.to.label === 'Back'; + +const transitionEndId = (t: Transition) => { + const endId = + t.to.__typename === 'WorkflowTransitionStaticTo' + ? t.to.state.value + : t.to.id; + return `${t.label} -> ${endId}`; +}; +const isDynamic = isTypename( + 'WorkflowTransitionDynamicTo' +); diff --git a/src/scenes/Projects/Workflow/Flowchart/workflow.graphql b/src/scenes/Projects/Workflow/Flowchart/workflow.graphql new file mode 100644 index 000000000..048d3e580 --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/workflow.graphql @@ -0,0 +1,45 @@ +fragment workflow on Workflow { + id + states { + ...workflowState + } + transitions { + ...workflowTransition + } +} + +fragment workflowState on WorkflowState { + value + label +} + +fragment workflowTransition on WorkflowTransition { + key + devName + label + type + from { + ...workflowState + } + to { + __typename + ... on WorkflowTransitionStaticTo { + state { + ...workflowState + } + } + ... on WorkflowTransitionDynamicTo { + id + label + relatedStates { + ...workflowState + } + } + } + conditions { + label + } + notifiers { + label + } +} diff --git a/yarn.lock b/yarn.lock index 100b4e214..fc755f278 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1690,6 +1690,22 @@ __metadata: languageName: node linkType: hard +"@dagrejs/dagre@npm:^1.1.2": + version: 1.1.2 + resolution: "@dagrejs/dagre@npm:1.1.2" + dependencies: + "@dagrejs/graphlib": "npm:2.2.2" + checksum: 10c0/717b3e6974b67a3839ea828228582fa3bd310fac5aadc4a68d5e4b96c3f7bcb97cb6f78518bdbd4c14e4fa1cc764ac6259fa567734916fa51f078f5747c85877 + languageName: node + linkType: hard + +"@dagrejs/graphlib@npm:2.2.2": + version: 2.2.2 + resolution: "@dagrejs/graphlib@npm:2.2.2" + checksum: 10c0/2e79a4f5c6c402054b7ef42e786459645495934476170999f13867a55a00072636a23914772cce6bc03ce51eef70de589058860b8f034c1d70804fb61e01fcfc + languageName: node + linkType: hard + "@date-io/core@npm:^2.15.0, @date-io/core@npm:^2.17.0": version: 2.17.0 resolution: "@date-io/core@npm:2.17.0" @@ -3846,6 +3862,102 @@ __metadata: languageName: node linkType: hard +"@reactflow/background@npm:11.3.13": + version: 11.3.13 + resolution: "@reactflow/background@npm:11.3.13" + dependencies: + "@reactflow/core": "npm:11.11.3" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/54929a506c1b73b6406d511a0a55a89cf88eb2073bc4e72defde63b3da46794ea87638e017180492798d717e9220e833f66c7419270137ace9acaa040f790e6e + languageName: node + linkType: hard + +"@reactflow/controls@npm:11.2.13": + version: 11.2.13 + resolution: "@reactflow/controls@npm:11.2.13" + dependencies: + "@reactflow/core": "npm:11.11.3" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/219285855f5a76ad77bf858e1eb3ed867d95f96b8c2f6480e6f9f89dce9e68617cd97565960455aa6fa9c190292ebf81f28da3a254abd413c358a0288773203c + languageName: node + linkType: hard + +"@reactflow/core@npm:11.11.3": + version: 11.11.3 + resolution: "@reactflow/core@npm:11.11.3" + dependencies: + "@types/d3": "npm:^7.4.0" + "@types/d3-drag": "npm:^3.0.1" + "@types/d3-selection": "npm:^3.0.3" + "@types/d3-zoom": "npm:^3.0.1" + classcat: "npm:^5.0.3" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/08c8353316c38ebc398e645f2e1e2d7246ea4e331485d604f8c6a8d54a83cc85e87921427b3fd7d161314258072b44508c9bba79faefa67e9d401e6b3f262b19 + languageName: node + linkType: hard + +"@reactflow/minimap@npm:11.7.13": + version: 11.7.13 + resolution: "@reactflow/minimap@npm:11.7.13" + dependencies: + "@reactflow/core": "npm:11.11.3" + "@types/d3-selection": "npm:^3.0.3" + "@types/d3-zoom": "npm:^3.0.1" + classcat: "npm:^5.0.3" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/71f89a7ae36ce6b50237401640043be20264fedfcd055f6a3e16cf84adcac343ad2f3fd74eb48875ab12538d3566aaddc21cce6160d602120e71797c79ee374e + languageName: node + linkType: hard + +"@reactflow/node-resizer@npm:2.2.13": + version: 2.2.13 + resolution: "@reactflow/node-resizer@npm:2.2.13" + dependencies: + "@reactflow/core": "npm:11.11.3" + classcat: "npm:^5.0.4" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/8fa01dc3c2805af56bfb93a05d7c2aecbf1c40fdd25d768ae0a0d252cec9ac04911493103abfbe3f8fcd840a8ea4a8c8342646eed7dcfcb3147d1de2a0661dff + languageName: node + linkType: hard + +"@reactflow/node-toolbar@npm:1.3.13": + version: 1.3.13 + resolution: "@reactflow/node-toolbar@npm:1.3.13" + dependencies: + "@reactflow/core": "npm:11.11.3" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/bfba042b97bcb11c11f71db3c3eab5f153fb2b666f6135d0658efb0bf2b6f210ecb52fa1da3af59eb995dd91d04fe223ef88af7c6e3bc349650fb93e17ed876c + languageName: node + linkType: hard + "@remix-run/router@npm:1.10.0": version: 1.10.0 resolution: "@remix-run/router@npm:1.10.0" @@ -4152,6 +4264,278 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.2.1 + resolution: "@types/d3-array@npm:3.2.1" + checksum: 10c0/38bf2c778451f4b79ec81a2288cb4312fe3d6449ecdf562970cc339b60f280f31c93a024c7ff512607795e79d3beb0cbda123bb07010167bce32927f71364bca + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.6 + resolution: "@types/d3-axis@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.6 + resolution: "@types/d3-brush@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.6 + resolution: "@types/d3-chord@npm:3.0.6" + checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae + languageName: node + linkType: hard + +"@types/d3-contour@npm:*": + version: 3.0.6 + resolution: "@types/d3-contour@npm:3.0.6" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.4 + resolution: "@types/d3-delaunay@npm:6.0.4" + checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.6 + resolution: "@types/d3-dispatch@npm:3.0.6" + checksum: 10c0/405eb7d0ec139fbf72fa6a43b0f3ca8a1f913bb2cb38f607827e63fca8d4393f021f32f3b96b33c93ddbd37789453a0b3624f14f504add5308fd9aec8a46dda0 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.1": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d + languageName: node + linkType: hard + +"@types/d3-dsv@npm:*": + version: 3.0.7 + resolution: "@types/d3-dsv@npm:3.0.7" + checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.7 + resolution: "@types/d3-fetch@npm:3.0.7" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.9 + resolution: "@types/d3-force@npm:3.0.9" + checksum: 10c0/6d791a48ea570daaada6df93af8c877d58e6b940b3ab4515cde08ed6ed1d4e8e59fd8407efe37a1b3f5fe95867fe83a2974c4314a7924dc19860a5e955c26211 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.1.0 + resolution: "@types/d3-geo@npm:3.1.0" + dependencies: + "@types/geojson": "npm:*" + checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*": + version: 3.1.7 + resolution: "@types/d3-hierarchy@npm:3.1.7" + checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 10c0/066ebb8da570b518dd332df6b12ae3b1eaa0a7f4f0c702e3c57f812cf529cc3500ec2aac8dc094f31897790346c6b1ebd8cd7a077176727f4860c2b181a65ca4 + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.1.0 + resolution: "@types/d3-path@npm:3.1.0" + checksum: 10c0/85e8b3aa968a60a5b33198ade06ae7ffedcf9a22d86f24859ff58e014b053ccb7141ec163b78d547bc8215bb12bb54171c666057ab6156912814005b686afb31 + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.2 + resolution: "@types/d3-polygon@npm:3.0.2" + checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.6 + resolution: "@types/d3-quadtree@npm:3.0.6" + checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.3 + resolution: "@types/d3-random@npm:3.0.3" + checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.0.3 + resolution: "@types/d3-scale-chromatic@npm:3.0.3" + checksum: 10c0/2f48c6f370edba485b57b73573884ded71914222a4580140ff87ee96e1d55ccd05b1d457f726e234a31269b803270ac95d5554229ab6c43c7e4a9894e20dd490 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.8 + resolution: "@types/d3-scale@npm:4.0.8" + dependencies: + "@types/d3-time": "npm:*" + checksum: 10c0/57de90e4016f640b83cb960b7e3a0ab3ed02e720898840ddc5105264ffcfea73336161442fdc91895377c2d2f91904d637282f16852b8535b77e15a761c8e99e + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.3": + version: 3.0.10 + resolution: "@types/d3-selection@npm:3.0.10" + checksum: 10c0/de1f99ab186a08999bf394a645fd76911add1b02316270d4c07616c8383903a2b068d7e02b73b6a99a1f26bb49a2e99ef4b55a5d2ddfa165f6f3c53144897920 + languageName: node + linkType: hard + +"@types/d3-shape@npm:*": + version: 3.1.6 + resolution: "@types/d3-shape@npm:3.1.6" + dependencies: + "@types/d3-path": "npm:*" + checksum: 10c0/0625715925d3c7ed3d44ce998b42c993f063c31605b6e4a8046c4be0fe724e2d214fc83e86d04f429a30a6e1f439053e92b0d9e59e1180c3a5327b4a6e79fa0a + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.3 + resolution: "@types/d3-time@npm:3.0.3" + checksum: 10c0/245a8aadca504df27edf730de502e47a68f16ae795c86b5ca35e7afa91c133aa9ef4d08778f8cf1ed2be732f89a4105ba4b437ce2afbdfd17d3d937b6ba5f568 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*": + version: 3.0.8 + resolution: "@types/d3-transition@npm:3.0.8" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/feba7845bd1e1d49e38b0d55562e01e90bfbcf0a56fbe0de4279c12e43a687032d22ed559629c0412145d25d61e4e53ddfef34c89c6bf043d48b6c2cd3a929dc + languageName: node + linkType: hard + +"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.1": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547 + languageName: node + linkType: hard + +"@types/d3@npm:^7.4.0": + version: 7.4.3 + resolution: "@types/d3@npm:7.4.3" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 + languageName: node + linkType: hard + "@types/eslint@npm:^8.44.6": version: 8.44.6 resolution: "@types/eslint@npm:8.44.6" @@ -4193,6 +4577,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.14 + resolution: "@types/geojson@npm:7946.0.14" + checksum: 10c0/54f3997708fa2970c03eeb31f7e4540a0eb6387b15e9f8a60513a1409c23cafec8d618525404573468b59c6fecbfd053724b3327f7fca416729c26271d799f55 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.2": version: 4.1.8 resolution: "@types/graceful-fs@npm:4.1.8" @@ -6887,6 +7278,13 @@ __metadata: languageName: node linkType: hard +"classcat@npm:^5.0.3, classcat@npm:^5.0.4": + version: 5.0.5 + resolution: "classcat@npm:5.0.5" + checksum: 10c0/ff8d273055ef9b518529cfe80fd0486f7057a9917373807ff802d75ceb46e8f8e148f41fa094ee7625c8f34642cfaa98395ff182d9519898da7cbf383d4a210d + languageName: node + linkType: hard + "clean-css@npm:^4.2.3": version: 4.2.4 resolution: "clean-css@npm:4.2.4" @@ -7406,6 +7804,7 @@ __metadata: "@babel/plugin-transform-runtime": "npm:^7.23.2" "@babel/runtime": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" + "@dagrejs/dagre": "npm:^1.1.2" "@date-io/core": "npm:^2.17.0" "@date-io/luxon": "npm:^2.17.0" "@editorjs/checklist": "npm:^1.5.0" @@ -7567,6 +7966,7 @@ __metadata: react-pdf: "npm:^5.7.2" react-router: "npm:^6.17.0" react-router-dom: "npm:^6.17.0" + reactflow: "npm:^11.11.3" response-time: "npm:^2.3.2" rifm: "npm:^0.12.1" rimraf: "npm:^5.0.5" @@ -8028,6 +8428,88 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 10c0/19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -16620,6 +17102,23 @@ __metadata: languageName: node linkType: hard +"reactflow@npm:^11.11.3": + version: 11.11.3 + resolution: "reactflow@npm:11.11.3" + dependencies: + "@reactflow/background": "npm:11.3.13" + "@reactflow/controls": "npm:11.2.13" + "@reactflow/core": "npm:11.11.3" + "@reactflow/minimap": "npm:11.7.13" + "@reactflow/node-resizer": "npm:2.2.13" + "@reactflow/node-toolbar": "npm:1.3.13" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/1de038357a3b1ec440a06f041540ec4738419366bff13829996407b963f69bec7acfd1dca475c067727146bfd8f6ce1b230ce499abdbd6a8a2a37493cf547a94 + languageName: node + linkType: hard + "read-pkg-up@npm:^7.0.1": version: 7.0.1 resolution: "read-pkg-up@npm:7.0.1" @@ -19645,7 +20144,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0": +"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" peerDependencies: @@ -20577,3 +21076,23 @@ __metadata: checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8 languageName: node linkType: hard + +"zustand@npm:^4.4.1": + version: 4.5.2 + resolution: "zustand@npm:4.5.2" + dependencies: + use-sync-external-store: "npm:1.2.0" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 10c0/aee26f11facebb39b016e89539f72a72c2c00151208907fc909c3cedd455728240e09e01d98ebd3b63a2a3518a5917eac5de6c853743ca55a1655296d750bb48 + languageName: node + linkType: hard From fcb23bc87362be4cb7354967071c72cbb1ed7721 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 10 Jun 2024 12:17:22 -0500 Subject: [PATCH 02/12] Auto layout without flashing or fixed timeouts --- .../Workflow/Flowchart/ProjectFlowchart.tsx | 44 +++++----------- .../Workflow/Flowchart/useAutoLayout.ts | 51 +++++++++++++++++++ 2 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx index 227ad339f..7acbd1fff 100644 --- a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@apollo/client'; -import { ComponentType, useEffect } from 'react'; +import { ComponentType } from 'react'; import ReactFlow, { Background, Controls, @@ -8,9 +8,7 @@ import ReactFlow, { ReactFlowProvider, useEdgesState, useNodesState, - useReactFlow, } from 'reactflow'; -import { determinePositions } from './layout'; import { Edge as EdgeComponent, FlowchartStyles, @@ -19,6 +17,7 @@ import { } from './nodes'; import { NodeTypes, parseWorkflow } from './parse-node-edges'; import { ProjectFlowchartDocument } from './ProjectFlowchart.graphql'; +import { useAutoLayout } from './useAutoLayout'; const WrappedFlowchart = () => ( @@ -29,39 +28,21 @@ const WrappedFlowchart = () => ( export default WrappedFlowchart; const ProjectFlowchart = () => { - const { fitView } = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const autoLayout = useAutoLayout(setNodes); - const { data } = useQuery(ProjectFlowchartDocument); - useEffect(() => { - const workflow = data?.workflow; - if (!workflow) { - return; - } - const { nodes, edges } = parseWorkflow(workflow); - - const positionedNodes = determinePositions(nodes, edges); - setNodes([...positionedNodes]); - setEdges([...edges]); - window.requestAnimationFrame(() => { - fitView(); - }); - - // TODO Better: render, to get size, then do layout - setTimeout(() => { - setNodes((prev) => { - const positionedNodes = determinePositions(prev, edges); - setTimeout(() => { - fitView(); - }, 100); - return positionedNodes; - }); - }, 100); - }, [data, fitView, setEdges, setNodes]); + useQuery(ProjectFlowchartDocument, { + onCompleted: ({ workflow }) => { + autoLayout.reset(); + const { nodes, edges } = parseWorkflow(workflow); + setNodes(nodes); + setEdges(edges); + }, + }); return ( - + { nodeTypes={nodeTypes} edgeTypes={edgeTypes} minZoom={0.1} - fitView nodesConnectable={false} > diff --git a/src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts b/src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts new file mode 100644 index 000000000..ba9eafa81 --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts @@ -0,0 +1,51 @@ +import { useToggle } from 'ahooks'; +import { Dispatch, useCallback, useEffect, useRef } from 'react'; +import { Node, useReactFlow, useStoreApi } from 'reactflow'; +import { Sx } from '~/common'; +import { determinePositions } from './layout'; + +export const useAutoLayout = (setNodes: Dispatch) => { + const api = useStoreApi(); + const { fitView, getNodes, getEdges } = useReactFlow(); + const autoLayoutStage = useRef(0); + const [show, setShow] = useToggle(); + + const reset = useCallback(() => { + autoLayoutStage.current = 0; + }, []); + + useEffect( + () => + api.subscribe((state) => { + if (autoLayoutStage.current === 0 && state.getNodes()[0]?.width) { + // console.log('nodes have sized, positioning'); + autoLayoutStage.current = 1; + const positioned = determinePositions(getNodes(), getEdges()); + setNodes(positioned); + return; + } + + if (autoLayoutStage.current === 1) { + // console.log('nodes have positioned, adjusting view port'); + autoLayoutStage.current = 2; + window.requestAnimationFrame(() => fitView()); + return; + } + + if (autoLayoutStage.current === 2) { + // console.log('viewport adjusted, showing'); + autoLayoutStage.current = 3; + setShow.setRight(); + } + }), + [fitView, api, autoLayoutStage, setShow, getNodes, getEdges, setNodes] + ); + + return { + show, + showSx: !show ? showSx : undefined, + reset, + }; +}; + +const showSx: Sx = { '.react-flow__renderer': { opacity: 0 } }; From 508c298aaf2f450833b7c4d75faafaf1259acc66 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 08:16:37 -0500 Subject: [PATCH 03/12] Display notifiers as tooltip on transition node --- .../Projects/Workflow/Flowchart/nodes.tsx | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx index 5c415302f..f65188a65 100644 --- a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx @@ -1,5 +1,6 @@ -import { Box, Card, CardProps } from '@mui/material'; +import { Box, Card, CardProps, Tooltip } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { forwardRef, Fragment } from 'react'; import { BezierEdge, EdgeProps, Handle, NodeProps, Position } from 'reactflow'; import { extendSx } from '~/common'; import { transitionTypeStyles } from '~/common/transitionTypeStyles'; @@ -69,48 +70,64 @@ export function TransitionNode({ data, selected }: NodeProps) { return ( <> - - {data.label} - + }> + + {data.label} + + ); } -const NodeCard = ({ - children, - selected, - color, - sx, -}: CardProps & Pick) => ( - ({ - transition: theme.transitions.create(['box-shadow', 'border-color'], { - duration: theme.transitions.duration.shorter, +const NodeCard = forwardRef< + HTMLDivElement, + CardProps & Pick +>(function NodeCard({ children, selected, color, sx, ...rest }, ref) { + return ( + ({ + transition: theme.transitions.create(['box-shadow', 'border-color'], { + duration: theme.transitions.duration.shorter, + }), + borderColor: selected ? `${color}.dark` : 'transparent', + borderWidth: 1, + borderStyle: 'solid', + p: 2, + bgcolor: `${color}.main`, + color: `${color}.contrastText`, }), - borderColor: selected ? `${color}.dark` : 'transparent', - borderWidth: 1, - borderStyle: 'solid', - p: 2, - bgcolor: `${color}.main`, - color: `${color}.contrastText`, - }), - ...extendSx(sx), - ]} - > - {children} - + ...extendSx(sx), + ]} + > + {children} + + ); +}); + +const Notifiers = (t: Transition) => ( + <> + Notifiers:
+ {t.notifiers.map((n) => ( + + - {n.label} +
+
+ ))} + ); export const Edge = (props: EdgeProps) => { From f01972eaf165bf0a7f13f5eccf9dfc8a7ff6593d Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 14:38:16 -0500 Subject: [PATCH 04/12] Use floating edges & hide handles Adapted from their example --- .../Projects/Workflow/Flowchart/layout.ts | 56 ++++++++++++++- .../Projects/Workflow/Flowchart/nodes.tsx | 71 ++++++++++++------- 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/layout.ts b/src/scenes/Projects/Workflow/Flowchart/layout.ts index de151592d..4d8573fbc 100644 --- a/src/scenes/Projects/Workflow/Flowchart/layout.ts +++ b/src/scenes/Projects/Workflow/Flowchart/layout.ts @@ -1,5 +1,5 @@ import Dagre from '@dagrejs/dagre'; -import { Edge, Node } from 'reactflow'; +import { Edge, Node, Position as Side, XYPosition } from 'reactflow'; const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); @@ -21,3 +21,57 @@ export const determinePositions = (nodes: Node[], edges: Edge[]) => { return { ...node, position: { x, y } }; }); }; + +/** + * Returns the intersection point of the line between the center of the + * intersectionNode and the target node + */ +export function getNodeIntersection( + intersectionNode: Node, + targetNode: Node, + margin: XYPosition = { x: 10, y: 0 } +) { + // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const w = intersectionNode.width! / 2 - margin.x; + const h = intersectionNode.height! / 2 - margin.y; + + const x2 = intersectionNode.positionAbsolute!.x + w + margin.x; + const y2 = intersectionNode.positionAbsolute!.y + h + margin.y; + const x1 = targetNode.positionAbsolute!.x + targetNode.width! / 2; + const y1 = targetNode.positionAbsolute!.y + targetNode.height! / 2; + + const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); + const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); + const a = 1 / (Math.abs(xx1) + Math.abs(yy1)); + const xx3 = a * xx1; + const yy3 = a * yy1; + const x = w * (xx3 + yy3) + x2; + const y = h * (-xx3 + yy3) + y2; + + return { x, y }; +} + +/** + * Returns the Side/"Position" of the edge where the intersection point is + */ +export function getEdgeSide(node: Node, intersectsAt: XYPosition) { + const n = { ...node.positionAbsolute, ...node }; + const nx = Math.round(n.x!); + const ny = Math.round(n.y!); + const px = Math.round(intersectsAt.x); + const py = Math.round(intersectsAt.y); + + if (px <= nx + 1) { + return Side.Left; + } + if (px >= nx + n.width! - 1) { + return Side.Right; + } + if (py <= ny + 1) { + return Side.Top; + } + if (py >= n.y! + n.height! - 1) { + return Side.Bottom; + } + return Side.Top; +} diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx index f65188a65..2808c5f57 100644 --- a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx @@ -1,9 +1,18 @@ import { Box, Card, CardProps, Tooltip } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { forwardRef, Fragment } from 'react'; -import { BezierEdge, EdgeProps, Handle, NodeProps, Position } from 'reactflow'; +import { forwardRef, Fragment, useCallback } from 'react'; +import { + BaseEdge, + EdgeProps, + getSimpleBezierPath as getPath, + Handle, + NodeProps, + Position, + useStore, +} from 'reactflow'; import { extendSx } from '~/common'; import { transitionTypeStyles } from '~/common/transitionTypeStyles'; +import { getEdgeSide, getNodeIntersection } from './layout'; import { isBack } from './parse-node-edges'; import { WorkflowStateFragment as State, @@ -20,16 +29,12 @@ export const FlowchartStyles = styled(Box)(({ theme }) => ({ fill: theme.palette.background.paper, }, '.react-flow__handle': { - top: '50%', - left: '50%', - right: 'auto', - bottom: 'auto', - transform: 'translate(-50%, -50%)', - background: 'transparent', - border: 'none', + opacity: 0, + pointerEvents: 'none', + cursor: 'unset', }, '.react-flow__edge': { - '.react-flow__edge-path, .react-flow__connection-path': { + '.react-flow__edge-path': { stroke: 'var(--color)', strokeWidth: 2, }, @@ -49,17 +54,8 @@ export function StateNode({ data, selected }: NodeProps) { {data.label} - - + + ); } @@ -130,14 +126,41 @@ const Notifiers = (t: Transition) => ( ); -export const Edge = (props: EdgeProps) => { +export const Edge = ({ + id, + source, + target, + ...props +}: EdgeProps) => { + const sourceNode = useStore( + useCallback((store) => store.nodeInternals.get(source)!, [source]) + ); + const targetNode = useStore( + useCallback((store) => store.nodeInternals.get(target)!, [target]) + ); + + const sourceIntersectsAt = getNodeIntersection(sourceNode, targetNode); + const targetIntersectsAt = getNodeIntersection(targetNode, sourceNode); + const [path, labelX, labelY, offsetX, offsetY] = getPath({ + sourceX: sourceIntersectsAt.x, + sourceY: sourceIntersectsAt.y, + sourcePosition: getEdgeSide(sourceNode, sourceIntersectsAt), + targetX: targetIntersectsAt.x, + targetY: targetIntersectsAt.y, + targetPosition: getEdgeSide(targetNode, targetIntersectsAt), + }); + const pathProps = { path, labelX, labelY, offsetX, offsetY }; + const { color } = transitionTypeStyles[props.data!.type]; return ( ({ '--color': theme.palette[color].light })} + id={id} + sx={(theme) => ({ + '--color': theme.palette[color][props.selected ? 'main' : 'light'], + })} > - + ); }; From dbcc1317cc0e90ec3e9562b4cb186b64610ae6e7 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 15:16:53 -0500 Subject: [PATCH 05/12] Tweak back edge styling --- src/scenes/Projects/Workflow/Flowchart/nodes.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx index 2808c5f57..8f8450aa7 100644 --- a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx @@ -151,13 +151,16 @@ export const Edge = ({ }); const pathProps = { path, labelX, labelY, offsetX, offsetY }; + const back = sourceNode.type === 'transition' && isBack(sourceNode.data); const { color } = transitionTypeStyles[props.data!.type]; return ( ({ - '--color': theme.palette[color][props.selected ? 'main' : 'light'], + '--color': back + ? theme.palette.grey[props.selected ? 600 : 300] + : theme.palette[color][props.selected ? 'main' : 'light'], })} > From 0f51d152614797937b8513e78445c1dfb955b80c Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 15:21:27 -0500 Subject: [PATCH 06/12] Adjust layout algorithm to try to make sense of the spaghetti --- .../Projects/Workflow/Flowchart/layout.ts | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/layout.ts b/src/scenes/Projects/Workflow/Flowchart/layout.ts index 4d8573fbc..2ab03db1a 100644 --- a/src/scenes/Projects/Workflow/Flowchart/layout.ts +++ b/src/scenes/Projects/Workflow/Flowchart/layout.ts @@ -1,22 +1,60 @@ import Dagre from '@dagrejs/dagre'; -import { Edge, Node, Position as Side, XYPosition } from 'reactflow'; +import { mapKeys, simpleSwitch } from '@seedcompany/common'; +import { Edge as E, Node as N, Position as Side, XYPosition } from 'reactflow'; +import { NodeTypes } from './parse-node-edges'; +import { + WorkflowStateFragment as State, + WorkflowTransitionFragment as Transition, +} from './workflow.graphql'; -const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); +type Node = N; +type Edge = E; export const determinePositions = (nodes: Node[], edges: Edge[]) => { - g.setGraph({ rankdir: 'TB' }); + const g = new Dagre.graphlib.Graph() + .setDefaultEdgeLabel(() => ({})) + .setGraph({ + ranksep: 80, + acyclicer: 'greedy', + }); - edges.forEach((edge) => g.setEdge(edge.source, edge.target)); - nodes.forEach((node) => g.setNode(node.id, node as any)); + const nodeMap = mapKeys.fromList(nodes, (n) => n.id).asMap; + + nodes.forEach((node) => + g.setNode(node.id, { + ...node, + width: node.width!, + height: node.height!, + }) + ); + edges.forEach((edge) => + g.setEdge(edge.source, edge.target, { + weight: + simpleSwitch(edge.data!.type, { + Approve: 10, + Neutral: 2, + Reject: 1, + })! + (nodeMap.get(edge.source)!.type === 'state' ? 2 : 0), + }) + ); Dagre.layout(g); + const max = { + x: Math.max(...nodes.map((n) => g.node(n.id).x)), + y: Math.max(...nodes.map((n) => g.node(n.id).y)), + }; + return nodes.map((node) => { const position = g.node(node.id); - // We are shifting the dagre node position (anchor=center center) to the top left - // so it matches the React Flow node anchor point (top left). - const x = position.x - (node.width ?? 0) / 2; - const y = position.y - (node.height ?? 0) / 2; + // Convert anchor point from dagre to react flow + // center/center -> top/left + let x = position.x - node.width! / 2; + const y = position.y - node.height! / 2; + + // Invert x-axis because for some reason, dagre puts the starting + // point on the right. + x = max.x - x; return { ...node, position: { x, y } }; }); From 93c606d144f0d43a7fbab3fcfea40c2aa0436551 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 16:15:16 -0500 Subject: [PATCH 07/12] Persist user position changes automatically --- .../Workflow/Flowchart/ProjectFlowchart.tsx | 34 +++++++++++++++++-- .../Projects/Workflow/Flowchart/layout.ts | 5 +++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx index 7acbd1fff..f1f09ba30 100644 --- a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx @@ -1,13 +1,18 @@ import { useQuery } from '@apollo/client'; -import { ComponentType } from 'react'; +import { mapEntries } from '@seedcompany/common'; +import { useDebounceFn, useLocalStorageState } from 'ahooks'; +import { ComponentType, useCallback } from 'react'; import ReactFlow, { + applyNodeChanges, Background, Controls, EdgeTypes, NodeProps, + OnNodesChange, ReactFlowProvider, useEdgesState, useNodesState, + XYPosition, } from 'reactflow'; import { Edge as EdgeComponent, @@ -28,7 +33,10 @@ const WrappedFlowchart = () => ( export default WrappedFlowchart; const ProjectFlowchart = () => { - const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [storedPos, setStoredPos] = useLocalStorageState< + Record + >('project-workflow-flowchart-node-position-map'); + const [nodes, setNodes] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const autoLayout = useAutoLayout(setNodes); @@ -36,11 +44,31 @@ const ProjectFlowchart = () => { onCompleted: ({ workflow }) => { autoLayout.reset(); const { nodes, edges } = parseWorkflow(workflow); - setNodes(nodes); + const persistedPosNodes = nodes.map((node) => + storedPos?.[node.id] ? { ...node, position: storedPos[node.id]! } : node + ); + setNodes(persistedPosNodes); setEdges(edges); }, }); + const persist = useDebounceFn((nextNodes: typeof nodes) => { + setStoredPos( + mapEntries(nextNodes, ({ id, position }) => [id, position]).asRecord + ); + }); + + const onNodesChange: OnNodesChange = useCallback( + (changes) => { + setNodes((prev) => { + const next = applyNodeChanges(changes, prev); + persist.run(next); + return next; + }); + }, + [setNodes, persist] + ); + return ( { }; return nodes.map((node) => { + // node position was persisted keep user placement. + if (node.position.x > 0 || node.position.y > 0) { + return node; + } + const position = g.node(node.id); // Convert anchor point from dagre to react flow // center/center -> top/left From d0f5f34411217cfc1a5a9ec764617db4caec7745 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 17:05:14 -0500 Subject: [PATCH 08/12] Move notifiers to popup icon --- src/components/PaperTooltip/PaperTooltip.tsx | 30 +++---- .../Projects/Workflow/Flowchart/nodes.tsx | 21 ++--- .../Workflow/Flowchart/transition-info.tsx | 85 +++++++++++++++++++ 3 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 src/scenes/Projects/Workflow/Flowchart/transition-info.tsx diff --git a/src/components/PaperTooltip/PaperTooltip.tsx b/src/components/PaperTooltip/PaperTooltip.tsx index e860ba0cb..7ce085b70 100644 --- a/src/components/PaperTooltip/PaperTooltip.tsx +++ b/src/components/PaperTooltip/PaperTooltip.tsx @@ -1,17 +1,17 @@ -import { Tooltip } from '@mui/material'; +import { Tooltip, TooltipProps } from '@mui/material'; import { styled } from '@mui/material/styles'; -export const PaperTooltip = styled(Tooltip)( - ({ theme: { palette, spacing, typography, shadows } }) => ({ - tooltip: { - backgroundColor: palette.background.paper, - color: palette.text.primary, - ...typography.body1, - padding: spacing(1), - boxShadow: shadows[8], - }, - arrow: { - color: palette.background.paper, - }, - }) -); +export const PaperTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme: { palette, spacing, typography, shadows } }) => ({ + '& .MuiTooltip-tooltip': { + backgroundColor: palette.background.paper, + color: palette.text.primary, + ...typography.body1, + padding: spacing(1), + boxShadow: shadows[8], + }, + '& .MuiTooltip-arrow': { + color: palette.background.paper, + }, +})); diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx index 8f8450aa7..b2b1f136d 100644 --- a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/nodes.tsx @@ -1,6 +1,6 @@ -import { Box, Card, CardProps, Tooltip } from '@mui/material'; +import { Box, Card, CardProps } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { forwardRef, Fragment, useCallback } from 'react'; +import { forwardRef, useCallback } from 'react'; import { BaseEdge, EdgeProps, @@ -14,6 +14,7 @@ import { extendSx } from '~/common'; import { transitionTypeStyles } from '~/common/transitionTypeStyles'; import { getEdgeSide, getNodeIntersection } from './layout'; import { isBack } from './parse-node-edges'; +import { TransitionNodeExtra } from './transition-info'; import { WorkflowStateFragment as State, WorkflowTransitionFragment as Transition, @@ -66,7 +67,7 @@ export function TransitionNode({ data, selected }: NodeProps) { return ( <> - }> + ) { > {data.label} - + ); @@ -114,18 +115,6 @@ const NodeCard = forwardRef< ); }); -const Notifiers = (t: Transition) => ( - <> - Notifiers:
- {t.notifiers.map((n) => ( - - - {n.label} -
-
- ))} - -); - export const Edge = ({ id, source, diff --git a/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx b/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx new file mode 100644 index 000000000..7748e0d4e --- /dev/null +++ b/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx @@ -0,0 +1,85 @@ +import { Notifications as NotificationIcon } from '@mui/icons-material'; +import { + ListItemText, + MenuItem, + MenuList, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import { forwardRef, ReactElement } from 'react'; +import { PaperTooltip } from '../../../../components/PaperTooltip'; +import { WorkflowTransitionFragment as Transition } from './workflow.graphql'; + +import 'reactflow/dist/style.css'; + +interface TransitionProp { + transition: Transition; +} + +export const TransitionNodeExtra = ({ + transition, + children, +}: TransitionProp & { + children: ReactElement; +}) => ( + + {children} + +); + +const TransitionNodeExtraMenu = forwardRef( + function TransitionNodeExtraMenu({ ownerState, transition, ...props }, ref) { + return ( + + + + ); + } +); + +const NotifiersInfo = ({ transition }: TransitionProp) => ( + } + sx={{ + '& .MuiTooltip-tooltip': { + padding: 0, + }, + }} + > + + +); + +const NotifierList = ({ transition }: TransitionProp) => ( + <> + + Notifiers + + + {transition.notifiers.map((notifier) => ( + + {notifier.label} + + ))} + + +); From db03b0f2b2828a6f6b2aa305b4348d4274bfeda1 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jun 2024 23:39:46 -0500 Subject: [PATCH 09/12] Add permission info to transition nodes --- .../Workflow/Flowchart/transition-info.tsx | 53 ++++++++++++++++++- .../Workflow/Flowchart/workflow.graphql | 6 +++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx b/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx index 7748e0d4e..92a6d50fb 100644 --- a/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx +++ b/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx @@ -1,5 +1,12 @@ -import { Notifications as NotificationIcon } from '@mui/icons-material'; import { + Check as CheckIcon, + Circle as CircleIcon, + Notifications as NotificationIcon, + Security as SecurityIcon, + Close as XIcon, +} from '@mui/icons-material'; +import { + ListItemIcon, ListItemText, MenuItem, MenuList, @@ -8,6 +15,7 @@ import { Typography, } from '@mui/material'; import { forwardRef, ReactElement } from 'react'; +import { RoleLabels } from '~/api/schema/enumLists'; import { PaperTooltip } from '../../../../components/PaperTooltip'; import { WorkflowTransitionFragment as Transition } from './workflow.graphql'; @@ -51,6 +59,7 @@ const TransitionNodeExtraMenu = forwardRef( return ( + ); } @@ -83,3 +92,45 @@ const NotifierList = ({ transition }: TransitionProp) => ( ); + +const PermissionInfo = ({ transition }: TransitionProp) => ( + } + sx={{ + '& .MuiTooltip-tooltip': { + padding: 0, + }, + }} + > + + +); + +const PermissionPopup = ({ transition }: TransitionProp) => ( + <> + + Permissions + + + {transition.permissions + .filter((p) => p.execute != null) + .map((p) => ( + + + {!p.execute ? ( + + ) : p.condition ? ( + + ) : ( + + )} + + + + ))} + + +); diff --git a/src/scenes/Projects/Workflow/Flowchart/workflow.graphql b/src/scenes/Projects/Workflow/Flowchart/workflow.graphql index 048d3e580..7389106e4 100644 --- a/src/scenes/Projects/Workflow/Flowchart/workflow.graphql +++ b/src/scenes/Projects/Workflow/Flowchart/workflow.graphql @@ -42,4 +42,10 @@ fragment workflowTransition on WorkflowTransition { notifiers { label } + permissions { + role + readEvent + execute + condition + } } From 33e77fbee1fe805e909fa5a1d72c3e81c0d2230b Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jun 2024 10:45:57 -0500 Subject: [PATCH 10/12] Abstract for other workflows --- .../Workflow/Flowchart.tsx} | 29 ++++++++++++++----- .../Workflow}/layout.ts | 0 .../Workflow}/nodes.tsx | 0 .../Workflow}/parse-node-edges.tsx | 0 .../Workflow}/transition-info.tsx | 2 +- .../Workflow}/useAutoLayout.ts | 0 .../Workflow}/workflow.graphql | 0 src/scenes/Projects/Projects.tsx | 9 ++---- .../{Flowchart => }/ProjectFlowchart.graphql | 0 .../Projects/Workflow/ProjectFlowchart.tsx | 6 ++++ 10 files changed, 31 insertions(+), 15 deletions(-) rename src/{scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx => components/Workflow/Flowchart.tsx} (76%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/layout.ts (100%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/nodes.tsx (100%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/parse-node-edges.tsx (100%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/transition-info.tsx (97%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/useAutoLayout.ts (100%) rename src/{scenes/Projects/Workflow/Flowchart => components/Workflow}/workflow.graphql (100%) rename src/scenes/Projects/Workflow/{Flowchart => }/ProjectFlowchart.graphql (100%) create mode 100644 src/scenes/Projects/Workflow/ProjectFlowchart.tsx diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx b/src/components/Workflow/Flowchart.tsx similarity index 76% rename from src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx rename to src/components/Workflow/Flowchart.tsx index f1f09ba30..16fbb39f8 100644 --- a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.tsx +++ b/src/components/Workflow/Flowchart.tsx @@ -1,7 +1,8 @@ -import { useQuery } from '@apollo/client'; +import { TypedDocumentNode as DocumentNode, useQuery } from '@apollo/client'; import { mapEntries } from '@seedcompany/common'; import { useDebounceFn, useLocalStorageState } from 'ahooks'; -import { ComponentType, useCallback } from 'react'; +import { OperationDefinitionNode } from 'graphql'; +import { ComponentType, useCallback, useMemo } from 'react'; import ReactFlow, { applyNodeChanges, Background, @@ -21,26 +22,38 @@ import { TransitionNode, } from './nodes'; import { NodeTypes, parseWorkflow } from './parse-node-edges'; -import { ProjectFlowchartDocument } from './ProjectFlowchart.graphql'; import { useAutoLayout } from './useAutoLayout'; +import { WorkflowFragment } from './workflow.graphql'; -const WrappedFlowchart = () => ( +interface Props { + doc: DocumentNode<{ workflow: WorkflowFragment }, Record>; +} + +const WrappedFlowchart = (props: Props) => ( - + ); // eslint-disable-next-line react/display-name,import/no-default-export export default WrappedFlowchart; -const ProjectFlowchart = () => { +const Flowchart = (props: Props) => { + const opName = useMemo( + () => + props.doc.definitions.find( + (d): d is OperationDefinitionNode => d.kind === 'OperationDefinition' + )!.name!.value, + [props.doc] + ); + const [storedPos, setStoredPos] = useLocalStorageState< Record - >('project-workflow-flowchart-node-position-map'); + >(`${opName}-flowchart-node-position-map`); const [nodes, setNodes] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const autoLayout = useAutoLayout(setNodes); - useQuery(ProjectFlowchartDocument, { + useQuery(props.doc, { onCompleted: ({ workflow }) => { autoLayout.reset(); const { nodes, edges } = parseWorkflow(workflow); diff --git a/src/scenes/Projects/Workflow/Flowchart/layout.ts b/src/components/Workflow/layout.ts similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/layout.ts rename to src/components/Workflow/layout.ts diff --git a/src/scenes/Projects/Workflow/Flowchart/nodes.tsx b/src/components/Workflow/nodes.tsx similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/nodes.tsx rename to src/components/Workflow/nodes.tsx diff --git a/src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx b/src/components/Workflow/parse-node-edges.tsx similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/parse-node-edges.tsx rename to src/components/Workflow/parse-node-edges.tsx diff --git a/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx b/src/components/Workflow/transition-info.tsx similarity index 97% rename from src/scenes/Projects/Workflow/Flowchart/transition-info.tsx rename to src/components/Workflow/transition-info.tsx index 92a6d50fb..2685ebb30 100644 --- a/src/scenes/Projects/Workflow/Flowchart/transition-info.tsx +++ b/src/components/Workflow/transition-info.tsx @@ -16,7 +16,7 @@ import { } from '@mui/material'; import { forwardRef, ReactElement } from 'react'; import { RoleLabels } from '~/api/schema/enumLists'; -import { PaperTooltip } from '../../../../components/PaperTooltip'; +import { PaperTooltip } from '../PaperTooltip'; import { WorkflowTransitionFragment as Transition } from './workflow.graphql'; import 'reactflow/dist/style.css'; diff --git a/src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts b/src/components/Workflow/useAutoLayout.ts similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/useAutoLayout.ts rename to src/components/Workflow/useAutoLayout.ts diff --git a/src/scenes/Projects/Workflow/Flowchart/workflow.graphql b/src/components/Workflow/workflow.graphql similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/workflow.graphql rename to src/components/Workflow/workflow.graphql diff --git a/src/scenes/Projects/Projects.tsx b/src/scenes/Projects/Projects.tsx index 0b3506c5d..5fd56e37c 100644 --- a/src/scenes/Projects/Projects.tsx +++ b/src/scenes/Projects/Projects.tsx @@ -38,12 +38,9 @@ const ChangeRequestList = loadable(() => import('./ChangeRequest/List'), { resolveComponent: (m) => m.ProjectChangeRequestList, }); -const ProjectFlowchart = loadable( - () => import('./Workflow/Flowchart/ProjectFlowchart'), - { - resolveComponent: (m) => m.default, - } -); +const ProjectFlowchart = loadable(() => import('./Workflow/ProjectFlowchart'), { + resolveComponent: (m) => m.ProjectFlowchart, +}); export const Projects = () => ( diff --git a/src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql b/src/scenes/Projects/Workflow/ProjectFlowchart.graphql similarity index 100% rename from src/scenes/Projects/Workflow/Flowchart/ProjectFlowchart.graphql rename to src/scenes/Projects/Workflow/ProjectFlowchart.graphql diff --git a/src/scenes/Projects/Workflow/ProjectFlowchart.tsx b/src/scenes/Projects/Workflow/ProjectFlowchart.tsx new file mode 100644 index 000000000..81198bd56 --- /dev/null +++ b/src/scenes/Projects/Workflow/ProjectFlowchart.tsx @@ -0,0 +1,6 @@ +import Flowchart from '../../../components/Workflow/Flowchart'; +import { ProjectFlowchartDocument } from './ProjectFlowchart.graphql'; + +export const ProjectFlowchart = () => ( + +); From e8ecf06c123bd57bd0ee333ad1e041f483c60a74 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jun 2024 11:42:59 -0500 Subject: [PATCH 11/12] Patch css min for md4 -> md5 --- ...-webpack-plugin-npm-1.3.0-c66c75884d.patch | 13 ++++++++++++ package.json | 1 + yarn.lock | 21 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch diff --git a/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch b/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch new file mode 100644 index 000000000..11ff47efd --- /dev/null +++ b/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch @@ -0,0 +1,13 @@ +diff --git a/dist/index.js b/dist/index.js +index f50f242ce99f8894457dfe573cb5d525b8cbb800..8aa3b6da76d413834a60c7947e351c994e0a585b 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -278,7 +278,7 @@ class CssMinimizerPlugin { + cssMinimizer: _package.default.version, + 'css-minimizer-webpack-plugin-options': this.options, + name, +- contentHash: _crypto.default.createHash('md4').update(input).digest('hex') ++ contentHash: _crypto.default.createHash('md5').update(input).digest('hex') + }, name); + } + } diff --git a/package.json b/package.json index 701d23020..d110ba172 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ }, "resolutions": { "@iarna/rtf-to-html/rtf-parser": "patch:rtf-parser@npm:1.3.3#.yarn/patches/rtf-parser-npm-1.3.3-c57888a546.patch", + "css-minimizer-webpack-plugin@npm:^1.2.0": "patch:css-minimizer-webpack-plugin@npm%3A1.3.0#~/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch", "razzle/react-refresh": "^0.13.0", "razzle/@pmmmwh/react-refresh-webpack-plugin": "^0.5.6", "react-dev-utils/browserslist": "^4.14.2", diff --git a/yarn.lock b/yarn.lock index fc755f278..8243ef058 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8194,7 +8194,7 @@ __metadata: languageName: node linkType: hard -"css-minimizer-webpack-plugin@npm:^1.2.0": +"css-minimizer-webpack-plugin@npm:1.3.0": version: 1.3.0 resolution: "css-minimizer-webpack-plugin@npm:1.3.0" dependencies: @@ -8213,6 +8213,25 @@ __metadata: languageName: node linkType: hard +"css-minimizer-webpack-plugin@patch:css-minimizer-webpack-plugin@npm%3A1.3.0#~/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch": + version: 1.3.0 + resolution: "css-minimizer-webpack-plugin@patch:css-minimizer-webpack-plugin@npm%3A1.3.0#~/.yarn/patches/css-minimizer-webpack-plugin-npm-1.3.0-c66c75884d.patch::version=1.3.0&hash=ea44b1" + dependencies: + cacache: "npm:^15.0.5" + cssnano: "npm:^4.1.10" + find-cache-dir: "npm:^3.3.1" + jest-worker: "npm:^26.3.0" + p-limit: "npm:^3.0.2" + schema-utils: "npm:^3.0.0" + serialize-javascript: "npm:^5.0.1" + source-map: "npm:^0.6.1" + webpack-sources: "npm:^1.4.3" + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + checksum: 10c0/708c2fa46c8a37bdfeff176edbe463a8b0dd6dd08257d7fd2056f470873bc7504b0b7777aae1f65c34ead3f26f54769874c6f98906ae1441a708827724d396cf + languageName: node + linkType: hard + "css-select-base-adapter@npm:^0.1.1": version: 0.1.1 resolution: "css-select-base-adapter@npm:0.1.1" From e9719a968ec2aef573aa8b1d1512f1959fa3bd41 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jun 2024 11:46:11 -0500 Subject: [PATCH 12/12] Fix css min missing peer dep --- .yarnrc.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.yarnrc.yml b/.yarnrc.yml index 81ab91d88..b5ba869a4 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -20,6 +20,9 @@ packageExtensions: '@whatwg-node/fetch@*': peerDependenciesMeta: '@types/node': { optional: true } + 'css-minimizer-webpack-plugin@*': + peerDependencies: + 'clean-css': '*' 'ahooks@*': peerDependencies: '@babel/runtime': '*'