From f47ee441b1a72b08814b660e2e342f2cbb0988f3 Mon Sep 17 00:00:00 2001 From: junkisai Date: Mon, 6 Jan 2025 21:48:00 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20=20add=20NodesProvider=20a?= =?UTF-8?q?nd=20context=20for=20managing=20nodes=20and=20edges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ERDRenderer/ERDContent/ERDContent.tsx | 67 +++++++--- .../ERDContent/ERDContentContext.tsx | 22 +++- .../RelatedTables/RelatedTables.tsx | 29 ++-- .../Toolbar/TidyUpButton/TidyUpButton.tsx | 9 +- .../ERDContent/useAutoLayout/useAutoLayout.ts | 14 +- .../ERDContent/useInitialAutoLayout.ts | 19 +-- .../ERDContent/useSyncHiddenNodesChange.ts | 21 ++- .../useSyncHighlightsActiveTableChange.ts | 33 ++++- .../components/ERDRenderer/ERDRenderer.tsx | 40 +++--- .../erd-core/src/providers/NodesProvider.tsx | 124 ++++++++++++++++++ .../packages/erd-core/src/providers/index.ts | 1 + 11 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 frontend/packages/erd-core/src/providers/NodesProvider.tsx diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx index 51639b023..853db7529 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx @@ -6,14 +6,21 @@ import { Background, BackgroundVariant, type Edge, + type EdgeChange, type Node, + type NodeChange, type NodeMouseHandler, type OnNodeDrag, ReactFlow, + applyEdgeChanges, + applyNodeChanges, useEdgesState, - useNodesState, } from '@xyflow/react' import { type FC, useCallback } from 'react' +import { + NodesProvider, + useNodesContext, +} from '../../../providers/NodesProvider' import styles from './ERDContent.module.css' import { ERDContentProvider, useERDContentContext } from './ERDContentContext' import { NonRelatedTableGroupNode } from './NonRelatedTableGroupNode' @@ -36,8 +43,6 @@ const edgeTypes = { } type Props = { - nodes: Node[] - edges: Edge[] enabledFeatures?: | { fitViewWhenActiveTableChange?: boolean | undefined @@ -46,28 +51,21 @@ type Props = { | undefined } -export const ERDContentInner: FC = ({ - nodes: _nodes, - edges: _edges, - enabledFeatures, -}) => { - const [nodes, setNodes, onNodesChange] = useNodesState(_nodes) - const [edges, setEdges, onEdgesChange] = useEdgesState(_edges) +export const ERDContentInner: FC = ({ enabledFeatures }) => { + const { nodes, setNodes, edges, setEdges } = useNodesContext() const { state: { loading }, } = useERDContentContext() const { tableName: activeTableName } = useUserEditingActiveStore() - useInitialAutoLayout( - nodes, - enabledFeatures?.initialFitViewToActiveTable ?? true, - ) + useInitialAutoLayout(enabledFeatures?.initialFitViewToActiveTable ?? true) useFitViewWhenActiveTableChange( enabledFeatures?.fitViewWhenActiveTableChange ?? true, ) useSyncHighlightsActiveTableChange() useSyncHiddenNodesChange() + console.log(nodes) const { version } = useVersion() const handleNodeClick = useCallback( (tableId: string) => { @@ -95,13 +93,20 @@ export const ERDContentInner: FC = ({ hoverTableName: id, }) - setEdges(updatedEdges) - setNodes(updatedNodes) + setEdges({ + type: 'UPDATE_EDGES', + payload: updatedEdges, + }) + setNodes({ + type: 'UPDATE_DATA', + payload: updatedNodes, + }) }, [edges, nodes, setNodes, setEdges, activeTableName], ) const handleMouseLeaveNode: NodeMouseHandler = useCallback(() => { + console.log('handleMouseLeaveNode') const { nodes: updatedNodes, edges: updatedEdges } = highlightNodesAndEdges( nodes, edges, @@ -111,8 +116,14 @@ export const ERDContentInner: FC = ({ }, ) - setEdges(updatedEdges) - setNodes(updatedNodes) + setEdges({ + type: 'UPDATE_EDGES', + payload: updatedEdges, + }) + setNodes({ + type: 'UPDATE_DATA', + payload: updatedNodes, + }) }, [edges, nodes, setNodes, setEdges, activeTableName]) const handleDragStopNode: OnNodeDrag = useCallback( @@ -132,6 +143,26 @@ export const ERDContentInner: FC = ({ [version], ) + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + setNodes({ + type: 'UPDATE_NODES', + payload: applyNodeChanges(changes, nodes), + }) + }, + [setNodes, nodes], + ) + + const onEdgesChange = useCallback( + (changes: EdgeChange[]) => { + setEdges({ + type: 'UPDATE_EDGES', + payload: applyEdgeChanges(changes, edges), + }) + }, + [setEdges, edges], + ) + const panOnDrag = [1, 2] return ( diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContentContext.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContentContext.tsx index 4158205bc..d6d85c38f 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContentContext.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContentContext.tsx @@ -6,14 +6,19 @@ import { useState, } from 'react' +type LayoutStatus = 'not-started' | 'in-progress' | 'complete' + type ERDContentContextState = { loading: boolean initializeComplete: boolean + // + // layoutStatus: LayoutStatus } type ERDContentContextActions = { setLoading: (loading: boolean) => void setInitializeComplete: (initializeComplete: boolean) => void + // setLayoutStatus: (layoutStatus: LayoutStatus) => void } type ERDContentConextValue = { @@ -25,10 +30,13 @@ const ERDContentContext = createContext({ state: { loading: true, initializeComplete: false, + // + // layoutStatus: 'not-started', }, actions: { setLoading: () => {}, setInitializeComplete: () => {}, + // setLayoutStatus: () => {}, }, }) @@ -37,12 +45,22 @@ export const useERDContentContext = () => useContext(ERDContentContext) export const ERDContentProvider: FC = ({ children }) => { const [loading, setLoading] = useState(true) const [initializeComplete, setInitializeComplete] = useState(false) + // + const [layoutStatus, setLayoutStatus] = useState('not-started') return ( {children} diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableDetail/RelatedTables/RelatedTables.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableDetail/RelatedTables/RelatedTables.tsx index 2c619c18f..5b096a948 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableDetail/RelatedTables/RelatedTables.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableDetail/RelatedTables/RelatedTables.tsx @@ -1,6 +1,6 @@ import { convertDBStructureToNodes } from '@/components/ERDRenderer/convertDBStructureToNodes' import { openRelatedTablesLogEvent } from '@/features/gtm/utils' -import { useVersion } from '@/providers' +import { NodesProvider, useNodesContext, useVersion } from '@/providers' import { replaceHiddenNodeIds, updateActiveTableName, @@ -8,7 +8,7 @@ import { } from '@/stores' import type { Table } from '@liam-hq/db-structure' import { GotoIcon, IconButton } from '@liam-hq/ui' -import { ReactFlowProvider, useReactFlow } from '@xyflow/react' +import { ReactFlowProvider } from '@xyflow/react' import { type FC, useCallback } from 'react' import { ERDContent } from '../../../ERDContent' import styles from './RelatedTables.module.css' @@ -25,11 +25,10 @@ export const RelatedTables: FC = ({ table }) => { dbStructure: extractedDBStructure, showMode: 'TABLE_NAME', }) - const { getNodes } = useReactFlow() + const { nodes: mainPaneNodes } = useNodesContext() const { version } = useVersion() const handleClick = useCallback(() => { const visibleNodeIds: string[] = nodes.map((node) => node.id) - const mainPaneNodes = getNodes() const hiddenNodeIds = mainPaneNodes .filter((node) => !visibleNodeIds.includes(node.id)) .map((node) => node.id) @@ -42,7 +41,7 @@ export const RelatedTables: FC = ({ table }) => { cliVer: version.version, appEnv: version.envName, }) - }, [nodes, getNodes, table.name, version]) + }, [nodes, mainPaneNodes, table.name, version]) return (
@@ -55,16 +54,16 @@ export const RelatedTables: FC = ({ table }) => { />
- - - + + + + +
) diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/Toolbar/TidyUpButton/TidyUpButton.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/Toolbar/TidyUpButton/TidyUpButton.tsx index 0e4979506..ad6059f2b 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/Toolbar/TidyUpButton/TidyUpButton.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/Toolbar/TidyUpButton/TidyUpButton.tsx @@ -3,15 +3,16 @@ import { useVersion } from '@/providers' import { useUserEditingStore } from '@/stores' import { IconButton, TidyUpIcon } from '@liam-hq/ui' import { ToolbarButton } from '@radix-ui/react-toolbar' -import { useReactFlow } from '@xyflow/react' import { type FC, useCallback } from 'react' +import { useNodesContext } from '../../../../../providers/NodesProvider' import { useAutoLayout } from '../../useAutoLayout' export const TidyUpButton: FC = () => { - const { getNodes, getEdges } = useReactFlow() + const { nodes, edges } = useNodesContext() const { handleLayout } = useAutoLayout() const { showMode } = useUserEditingStore() const { version } = useVersion() + const handleClick = useCallback(() => { version.displayedOn === 'cli' && toolbarActionLogEvent({ @@ -20,8 +21,8 @@ export const TidyUpButton: FC = () => { cliVer: version.version, appEnv: version.envName, }) - handleLayout(getNodes(), getEdges()) - }, [handleLayout, showMode, getNodes, getEdges, version]) + handleLayout(nodes, edges) + }, [showMode, nodes, edges, version, handleLayout]) return ( diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useAutoLayout/useAutoLayout.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useAutoLayout/useAutoLayout.ts index abf21c20b..ff43980c5 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useAutoLayout/useAutoLayout.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useAutoLayout/useAutoLayout.ts @@ -1,3 +1,4 @@ +import { useNodesContext } from '@/providers' import { useReactFlow } from '@xyflow/react' import type { Edge, FitViewOptions, Node } from '@xyflow/react' import { useCallback } from 'react' @@ -5,7 +6,8 @@ import { useERDContentContext } from '../ERDContentContext' import { getElkLayout } from './getElkLayout' export const useAutoLayout = () => { - const { setNodes, fitView } = useReactFlow() + const { setNodes } = useNodesContext() + const { fitView } = useReactFlow() const { actions: { setLoading, setInitializeComplete }, } = useERDContentContext() @@ -38,14 +40,18 @@ export const useAutoLayout = () => { edges: visibleEdges, }) - setNodes([...hiddenNodes, ...newNodes]) + setNodes({ + type: 'UPDATE_SIZE_AND_POSITION', + payload: [...hiddenNodes, ...newNodes], + }) + setTimeout(() => { fitView(fitViewOptions) setLoading(false) setInitializeComplete(true) - }, 0) + }, 1) }, - [setNodes, fitView, setLoading, setInitializeComplete], + [setNodes, setLoading, setInitializeComplete, fitView], ) return { handleLayout } diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useInitialAutoLayout.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useInitialAutoLayout.ts index 2ae3c6aef..03a2b92b2 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useInitialAutoLayout.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useInitialAutoLayout.ts @@ -1,7 +1,7 @@ +import { useNodesContext } from '@/providers' import type { QueryParam } from '@/schemas/queryParam' import { addHiddenNodeIds, updateActiveTableName } from '@/stores' import { decompressFromEncodedURIComponent } from '@/utils' -import { type Node, useReactFlow } from '@xyflow/react' import { useEffect, useMemo } from 'react' import { useERDContentContext } from './ERDContentContext' import { highlightNodesAndEdges } from './highlightNodesAndEdges' @@ -26,10 +26,9 @@ const getHiddenNodeIdsFromUrl = async (): Promise => { return hiddenNodeIds ? hiddenNodeIds.split(',') : [] } -export const useInitialAutoLayout = ( - nodes: Node[], - shouldFitViewToActiveTable: boolean, -) => { +export const useInitialAutoLayout = (shouldFitViewToActiveTable: boolean) => { + const { nodes, edges } = useNodesContext() + const tableNodesInitialized = useMemo( () => nodes @@ -37,7 +36,6 @@ export const useInitialAutoLayout = ( .some((node) => node.measured), [nodes], ) - const { getEdges } = useReactFlow() const { state: { initializeComplete }, @@ -46,7 +44,7 @@ export const useInitialAutoLayout = ( useEffect(() => { const initialize = async () => { - if (initializeComplete) { + if (initializeComplete || !tableNodesInitialized) { return } @@ -54,7 +52,6 @@ export const useInitialAutoLayout = ( updateActiveTableName(activeTableName) const hiddenNodeIds = await getHiddenNodeIdsFromUrl() addHiddenNodeIds(hiddenNodeIds) - const edges = getEdges() const hiddenNodes = nodes.map((node) => ({ ...node, hidden: hiddenNodeIds.includes(node.id), @@ -67,9 +64,7 @@ export const useInitialAutoLayout = ( ? { maxZoom: 1, duration: 300, nodes: [{ id: activeTableName }] } : undefined - if (tableNodesInitialized) { - handleLayout(updatedNodes, updatedEdges, fitViewOptions) - } + handleLayout(updatedNodes, updatedEdges, fitViewOptions) } initialize() @@ -78,7 +73,7 @@ export const useInitialAutoLayout = ( initializeComplete, handleLayout, nodes, - getEdges, + edges, shouldFitViewToActiveTable, ]) } diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHiddenNodesChange.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHiddenNodesChange.ts index afa7f8675..bcd1000e0 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHiddenNodesChange.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHiddenNodesChange.ts @@ -1,6 +1,7 @@ import { useUserEditingStore } from '@/stores' -import { type Node, useReactFlow } from '@xyflow/react' -import { useEffect } from 'react' +import type { Node } from '@xyflow/react' +import { useEffect, useMemo } from 'react' +import { useNodesContext } from '../../../providers/NodesProvider' import { NON_RELATED_TABLE_GROUP_NODE_ID } from '../convertDBStructureToNodes' import { useERDContentContext } from './ERDContentContext' @@ -22,14 +23,17 @@ export const useSyncHiddenNodesChange = () => { const { state: { initializeComplete }, } = useERDContentContext() - const { getNodes, setNodes } = useReactFlow() + const { nodes, setNodes } = useNodesContext() const { hiddenNodeIds } = useUserEditingStore() + const shouldUpdate = useMemo(() => { + return nodes.some((node) => node.hidden !== hiddenNodeIds.has(node.id)) + }, [hiddenNodeIds, nodes]) + useEffect(() => { - if (!initializeComplete) { + if (!initializeComplete || !shouldUpdate) { return } - const nodes = getNodes() const updatedNodes: Node[] = nodes.map((node) => { const hidden = hiddenNodeIds.has(node.id) return { ...node, hidden } @@ -39,6 +43,9 @@ export const useSyncHiddenNodesChange = () => { updatedNodes.push(nonRelatedTableGroupNode) } - setNodes(updatedNodes) - }, [initializeComplete, getNodes, setNodes, hiddenNodeIds]) + setNodes({ + type: 'UPDATE_HIDDEN', + payload: updatedNodes, + }) + }, [initializeComplete, shouldUpdate, nodes, setNodes, hiddenNodeIds]) } diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts index d1eaebf89..5001a3940 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts @@ -1,6 +1,6 @@ import { useUserEditingActiveStore } from '@/stores' -import { useReactFlow } from '@xyflow/react' import { useEffect } from 'react' +import { useNodesContext } from '../../../providers/NodesProvider' import { useERDContentContext } from './ERDContentContext' import { highlightNodesAndEdges } from './highlightNodesAndEdges' @@ -8,7 +8,7 @@ export const useSyncHighlightsActiveTableChange = () => { const { state: { initializeComplete }, } = useERDContentContext() - const { getNodes, setNodes, getEdges, setEdges } = useReactFlow() + const { nodes, edges, setNodes, setEdges } = useNodesContext() const { tableName } = useUserEditingActiveStore() useEffect(() => { @@ -16,15 +16,34 @@ export const useSyncHighlightsActiveTableChange = () => { return } - const nodes = getNodes() - const edges = getEdges() const { nodes: updatedNodes, edges: updatedEdges } = highlightNodesAndEdges( nodes, edges, { activeTableName: tableName }, ) - setEdges(updatedEdges) - setNodes(updatedNodes) - }, [initializeComplete, tableName, getNodes, getEdges, setNodes, setEdges]) + const shouldUpdateNodes = nodes.some((node, index) => { + const updatedNode = updatedNodes[index] + return JSON.stringify(node.data) !== JSON.stringify(updatedNode?.data) + }) + + if (shouldUpdateNodes) { + setNodes({ + type: 'UPDATE_DATA', + payload: updatedNodes, + }) + } + + const shouldUpdateEdges = edges.some((edge, index) => { + const updatedEdge = updatedEdges[index] + return JSON.stringify(edge) !== JSON.stringify(updatedEdge) + }) + + if (shouldUpdateEdges) { + setEdges({ + type: 'UPDATE_EDGES', + payload: updatedEdges, + }) + } + }, [initializeComplete, tableName, nodes, edges, setNodes, setEdges]) } diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDRenderer.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDRenderer.tsx index 183276fda..f1f8a94d9 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDRenderer.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDRenderer.tsx @@ -8,7 +8,7 @@ import styles from './ERDRenderer.module.css' import { LeftPane } from './LeftPane' import '@/styles/globals.css' import { toggleLogEvent } from '@/features/gtm/utils' -import { useVersion } from '@/providers' +import { NodesProvider, useVersion } from '@/providers' import { useDBStructureStore, useUserEditingStore } from '@/stores' import { CardinalityMarkers } from './CardinalityMarkers' // biome-ignore lint/nursery/useImportRestrictions: Fixed in the next PR. @@ -53,27 +53,25 @@ export const ERDRenderer: FC = ({ defaultSidebarOpen = false }) => { - -
- -
-
- -
- - -
- + + +
+ +
+
+
- - -
-
-
+ + +
+ +
+ +
+
+
+
+
diff --git a/frontend/packages/erd-core/src/providers/NodesProvider.tsx b/frontend/packages/erd-core/src/providers/NodesProvider.tsx new file mode 100644 index 000000000..f5e68b190 --- /dev/null +++ b/frontend/packages/erd-core/src/providers/NodesProvider.tsx @@ -0,0 +1,124 @@ +import type { Edge, Node } from '@xyflow/react' +import { + type Dispatch, + type FC, + type PropsWithChildren, + createContext, + useContext, + useReducer, +} from 'react' +import { match } from 'ts-pattern' + +type NodesAction = + | { + type: 'UPDATE_NODES' + payload: Node[] + } + | { + type: 'UPDATE_HIDDEN' + payload: Node[] + } + | { + type: 'UPDATE_SIZE_AND_POSITION' + payload: Node[] + } + | { + type: 'UPDATE_DATA' + payload: Node[] + } + +const nodesReducer = (state: Node[], action: NodesAction): Node[] => { + console.log('nodesReducer') + console.log(action) + const result = match(action) + .with({ type: 'UPDATE_NODES' }, (action) => action.payload) + .with({ type: 'UPDATE_HIDDEN' }, (action) => { + return state.map((node) => { + const update = action.payload.find((update) => update.id === node.id) + if (!update) return node + + return { + ...node, + hidden: update.hidden ?? false, + } + }) + }) + .with({ type: 'UPDATE_SIZE_AND_POSITION' }, (action) => { + return state.map((node) => { + const update = action.payload.find((update) => update.id === node.id) + if (!update) return node + + return { + ...node, + position: update.position, + width: update.width ?? 0, + height: update.height ?? 0, + } + }) + }) + .with({ type: 'UPDATE_DATA' }, (action) => { + return state.map((node) => { + const update = action.payload.find((update) => update.id === node.id) + if (!update) return node + + return { + ...node, + data: { + ...node.data, + ...update.data, + }, + } + }) + }) + .otherwise(() => state) + + console.log('result') + console.log(result) + return result +} + +type EdgesAction = { + type: 'UPDATE_EDGES' + payload: Edge[] +} + +const edgesReducer = (state: Edge[], action: EdgesAction): Edge[] => { + return match(action) + .with({ type: 'UPDATE_EDGES' }, (action) => action.payload) + .otherwise(() => state) +} + +const NodesContext = createContext<{ + nodes: Node[] + edges: Edge[] + setNodes: Dispatch + setEdges: Dispatch +} | null>(null) + +type Props = { + nodes: Node[] + edges: Edge[] +} + +export const NodesProvider: FC> = ({ + nodes: _nodes, + edges: _edges, + children, +}) => { + const [nodes, setNodes] = useReducer(nodesReducer, _nodes) + const [edges, setEdges] = useReducer(edgesReducer, _edges) + + return ( + + {children} + + ) +} + +export const useNodesContext = () => { + const context = useContext(NodesContext) + if (!context) { + throw new Error('useNodesContext must be used within a NodesProvider') + } + return context +} diff --git a/frontend/packages/erd-core/src/providers/index.ts b/frontend/packages/erd-core/src/providers/index.ts index bfbc33767..0ed2d3756 100644 --- a/frontend/packages/erd-core/src/providers/index.ts +++ b/frontend/packages/erd-core/src/providers/index.ts @@ -1 +1,2 @@ +export * from './NodesProvider' export * from './versionProvider'