diff --git a/frontend/.changeset/big-impalas-call.md b/frontend/.changeset/big-impalas-call.md new file mode 100644 index 00000000..8a250ea0 --- /dev/null +++ b/frontend/.changeset/big-impalas-call.md @@ -0,0 +1,6 @@ +--- +"@liam-hq/erd-core": patch +"@liam-hq/cli": patch +--- + +refactor: integrate highlightNodesAndEdges function for improved node and edge highlighting on hover 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 632d4732..1f6c1684 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx @@ -1,4 +1,8 @@ -import { updateActiveTableName, useDBStructureStore } from '@/stores' +import { + updateActiveTableName, + useDBStructureStore, + useUserEditingActiveStore, +} from '@/stores' import type { Relationships } from '@liam-hq/db-structure' import { Background, @@ -10,12 +14,13 @@ import { useEdgesState, useNodesState, } from '@xyflow/react' -import { type FC, useCallback, useState } from 'react' +import { type FC, useCallback } from 'react' import styles from './ERDContent.module.css' import { ERDContentProvider, useERDContentContext } from './ERDContentContext' import { RelationshipEdge } from './RelationshipEdge' import { Spinner } from './Spinner' import { TableNode } from './TableNode' +import { highlightNodesAndEdges } from './highlightNodesAndEdges' import { useFitViewWhenActiveTableChange } from './useFitViewWhenActiveTableChange' import { useInitialAutoLayout } from './useInitialAutoLayout' import { useSyncHighlightsActiveTableChange } from './useSyncHighlightsActiveTableChange' @@ -39,18 +44,6 @@ type Props = { | undefined } -const highlightEdge = (edge: Edge): Edge => ({ - ...edge, - animated: true, - data: { ...edge.data, isHighlighted: true }, -}) - -const unhighlightEdge = (edge: Edge): Edge => ({ - ...edge, - animated: false, - data: { ...edge.data, isHighlighted: false }, -}) - export const isRelatedToTable = ( relationships: Relationships, tableName: string, @@ -68,22 +61,6 @@ export const isRelatedToTable = ( ) } -const getHighlightedHandles = ( - edges: Edge[], - targetId: string, - nodeId: string, -) => { - const highlightedTargetHandles = edges - .filter((edge) => edge.source === targetId && edge.target === nodeId) - .map((edge) => edge.targetHandle) - - const highlightedSourceHandles = edges - .filter((edge) => edge.target === targetId && edge.source === nodeId) - .map((edge) => edge.sourceHandle) - - return highlightedTargetHandles.concat(highlightedSourceHandles) || [] -} - export const ERDContentInner: FC = ({ nodes: _nodes, edges: _edges, @@ -95,8 +72,7 @@ export const ERDContentInner: FC = ({ const { state: { loading }, } = useERDContentContext() - // TODO: remove activeNodeId state - const [activeNodeId, setActiveNodeId] = useState(null) + const { tableName: activeTableName } = useUserEditingActiveStore() useUpdateNodeCardinalities(nodes, relationships, setNodes) useInitialAutoLayout() @@ -106,164 +82,40 @@ export const ERDContentInner: FC = ({ useSyncHighlightsActiveTableChange() const handleNodeClick = useCallback((nodeId: string) => { - setActiveNodeId(nodeId) updateActiveTableName(nodeId) }, []) const handlePaneClick = useCallback(() => { - setActiveNodeId(null) updateActiveTableName(undefined) }, []) const handleMouseEnterNode: NodeMouseHandler = useCallback( (_, { id }) => { - const relatedEdges = edges.filter( - (e) => e.source === id || e.target === id, - ) - - const relatedToActiveNodeEdges = edges.filter( - (e) => e.source === activeNodeId || e.target === activeNodeId, - ) - - const updatedEdges = edges.map((e) => { - if (relatedEdges.includes(e)) { - return highlightEdge(e) - } - if (relatedToActiveNodeEdges.includes(e)) { - return highlightEdge(e) - } - return unhighlightEdge(e) - }) - - const updatedNodes = nodes.map((node) => { - if (node.id === id) { - return { ...node, data: { ...node.data, isHighlighted: true } } - } - - const isRelated = isRelatedToTable(relationships, node.id, id) - const isRelatedToActiveNode = - activeNodeId && isRelatedToTable(relationships, node.id, activeNodeId) - - if (isRelated) { - let highlightedHandles = getHighlightedHandles(edges, id, node.id) - if (isRelatedToActiveNode) { - highlightedHandles = highlightedHandles.concat( - getHighlightedHandles(edges, activeNodeId, node.id), - ) - } - - return { - ...node, - data: { - ...node.data, - isHighlighted: true, - highlightedHandles: highlightedHandles, - }, - } - } - if (isRelatedToActiveNode) { - const highlightedHandles = getHighlightedHandles( - edges, - activeNodeId, - node.id, - ).concat(getHighlightedHandles(edges, id, node.id)) - - const isHighlighted = node.id === activeNodeId - - return { - ...node, - data: { - ...node.data, - isHighlighted: isHighlighted, - highlightedHandles: highlightedHandles, - }, - } - } - - return { - ...node, - data: { - ...node.data, - isHighlighted: false, - highlightedHandles: [], - }, - } - }) + const { nodes: updatedNodes, edges: updatedEdges } = + highlightNodesAndEdges(nodes, edges, { + activeTableName, + hoverTableName: id, + }) setEdges(updatedEdges) setNodes(updatedNodes) }, - [edges, nodes, setNodes, setEdges, relationships, activeNodeId], + [edges, nodes, setNodes, setEdges, activeTableName], ) - const handleMouseLeaveNode: NodeMouseHandler = useCallback( - (_, { id }) => { - // If a node is active, do not remove the highlight - if (activeNodeId) { - const relatedEdges = edges.filter( - (e) => e.source === activeNodeId || e.target === activeNodeId, - ) - const updatedEdges = edges.map((e) => - relatedEdges.includes(e) ? highlightEdge(e) : unhighlightEdge(e), - ) - setEdges(updatedEdges) - - const updatedNodes = nodes.map((node) => { - const isRelated = isRelatedToTable( - relationships, - node.id, - activeNodeId, - ) - - if (node.id === activeNodeId || isRelated) { - const highlightedHandles = getHighlightedHandles( - edges, - activeNodeId, - node.id, - ) - - const isHighlighted = node.id === activeNodeId - - return { - ...node, - data: { - ...node.data, - isHighlighted, - highlightedHandles: highlightedHandles, - }, - } - } - - return { - ...node, - data: { - ...node.data, - isHighlighted: false, - highlightedHandles: [], - }, - } - }) - - setNodes(updatedNodes) - } else { - const updatedEdges = edges.map((e) => - e.source === id || e.target === id ? unhighlightEdge(e) : e, - ) - setEdges(updatedEdges) - - const updatedNodes = nodes.map((node) => ({ - ...node, - data: { - ...node.data, - highlightedHandles: [], - isHighlighted: false, - }, - })) - setNodes(updatedNodes) - } - }, - [edges, nodes, setNodes, setEdges, activeNodeId, relationships], - ) + const handleMouseLeaveNode: NodeMouseHandler = useCallback(() => { + const { nodes: updatedNodes, edges: updatedEdges } = highlightNodesAndEdges( + nodes, + edges, + { + activeTableName, + hoverTableName: undefined, + }, + ) + + setEdges(updatedEdges) + setNodes(updatedNodes) + }, [edges, nodes, setNodes, setEdges, activeTableName]) const panOnDrag = [1, 2] diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.test.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.test.ts index 518c8173..0aa37926 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.test.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.test.ts @@ -62,11 +62,9 @@ describe(highlightNodesAndEdges, () => { describe('nodes', () => { it('When the users is active, the users and related tables are highlighted', () => { - const { nodes: updatedNodes } = highlightNodesAndEdges( - nodes, - edges, - 'users', - ) + const { nodes: updatedNodes } = highlightNodesAndEdges(nodes, edges, { + activeTableName: 'users', + }) expect(updatedNodes).toEqual([ aTableNode('users', { @@ -89,11 +87,9 @@ describe(highlightNodesAndEdges, () => { }) it('When no active table, no tables are highlighted', () => { - const { nodes: updatedNodes } = highlightNodesAndEdges( - nodes, - edges, - undefined, - ) + const { nodes: updatedNodes } = highlightNodesAndEdges(nodes, edges, { + activeTableName: undefined, + }) expect(updatedNodes).toEqual([ aTableNode('users', { @@ -114,15 +110,64 @@ describe(highlightNodesAndEdges, () => { }), ]) }) + it('When the users is hovered, the users and related tables are highlighted', () => { + const { nodes: updatedNodes } = highlightNodesAndEdges(nodes, edges, { + hoverTableName: 'users', + }) + + expect(updatedNodes).toEqual([ + aTableNode('users', { + data: aTableData('users', { isHighlighted: true }), + }), + aTableNode('posts', { + data: aTableData('posts', { + isHighlighted: true, + highlightedHandles: ['posts-user_id'], + }), + }), + aTableNode('comments'), + aTableNode('comment_users', { + data: aTableData('comment_users', { + isHighlighted: true, + highlightedHandles: ['comment_users-user_id'], + }), + }), + ]) + }) + it('When the users is active, and the comments is hovered, then the users and comments and related tables are highlighted', () => { + const { nodes: updatedNodes } = highlightNodesAndEdges(nodes, edges, { + activeTableName: 'users', + hoverTableName: 'comments', + }) + + expect(updatedNodes).toEqual([ + aTableNode('users', { + data: aTableData('users', { isActiveHighlighted: true }), + }), + aTableNode('posts', { + data: aTableData('posts', { + isHighlighted: true, + highlightedHandles: ['posts-user_id'], + }), + }), + aTableNode('comments', { + data: aTableData('comments', { isHighlighted: true }), + }), + aTableNode('comment_users', { + data: aTableData('comment_users', { + isHighlighted: true, + highlightedHandles: ['comment_users-user_id'], + }), + }), + ]) + }) }) describe('edges', () => { it('When the users is active, the users and related edges are highlighted', () => { - const { edges: updatedEdges } = highlightNodesAndEdges( - nodes, - edges, - 'users', - ) + const { edges: updatedEdges } = highlightNodesAndEdges(nodes, edges, { + activeTableName: 'users', + }) expect(updatedEdges).toEqual([ anEdge('users', 'posts', 'users-id', 'posts-user_id', { @@ -143,11 +188,9 @@ describe(highlightNodesAndEdges, () => { }) it('When no active table, no edges are highlighted', () => { - const { edges: updatedEdges } = highlightNodesAndEdges( - nodes, - edges, - undefined, - ) + const { edges: updatedEdges } = highlightNodesAndEdges(nodes, edges, { + activeTableName: undefined, + }) expect(updatedEdges).toEqual([ anEdge('users', 'posts', 'users-id', 'posts-user_id', { @@ -166,5 +209,54 @@ describe(highlightNodesAndEdges, () => { ), ]) }) + it('When the users is hovered, the users and related edges are highlighted', () => { + const { edges: updatedEdges } = highlightNodesAndEdges(nodes, edges, { + hoverTableName: 'users', + }) + + expect(updatedEdges).toEqual([ + anEdge('users', 'posts', 'users-id', 'posts-user_id', { + animated: true, + data: { isHighlighted: true }, + }), + anEdge('users', 'comment_users', 'users-id', 'comment_users-user_id', { + animated: true, + data: { isHighlighted: true }, + }), + anEdge( + 'comments', + 'comment_users', + 'comments-id', + 'comment_users-comment_id', + ), + ]) + }) + it('When the users is active, and the comments is hovered, then the users and comments and related edges are highlighted', () => { + const { edges: updatedEdges } = highlightNodesAndEdges(nodes, edges, { + activeTableName: 'users', + hoverTableName: 'comments', + }) + + expect(updatedEdges).toEqual([ + anEdge('users', 'posts', 'users-id', 'posts-user_id', { + animated: true, + data: { isHighlighted: true }, + }), + anEdge('users', 'comment_users', 'users-id', 'comment_users-user_id', { + animated: true, + data: { isHighlighted: true }, + }), + anEdge( + 'comments', + 'comment_users', + 'comments-id', + 'comment_users-comment_id', + { + animated: true, + data: { isHighlighted: true }, + }, + ), + ]) + }) }) }) diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts index cef0ea4b..09916e7b 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts @@ -12,31 +12,38 @@ const isActiveNode = ( return node.data.table.name === activeTableName } -const isActivelyRelatedNode = ( - activeTableName: string | undefined, +const isRelatedNodeToTarget = ( + targetTableName: string | undefined, edgeMap: EdgeMap, node: TableNodeType, ): boolean => { - if (!activeTableName) { + if (!targetTableName) { return false } - return edgeMap.get(activeTableName)?.includes(node.data.table.name) ?? false + return edgeMap.get(targetTableName)?.includes(node.data.table.name) ?? false } -const isActivelyRelatedEdge = ( - activeTableName: string | undefined, +const isHoveredNode = ( + hoverTableName: string | undefined, + node: TableNodeType, +): boolean => { + return node.data.table.name === hoverTableName +} + +const isRelatedEdgeToTarget = ( + targetTableName: string | undefined, edge: Edge, ): boolean => { - return edge.source === activeTableName || edge.target === activeTableName + return edge.source === targetTableName || edge.target === targetTableName } const getHighlightedHandlesForRelatedNode = ( - activeTableName: string | undefined, + targetTableName: string | undefined, edges: Edge[], node: TableNodeType, ): string[] => { - if (!activeTableName) { + if (!targetTableName) { return [] } @@ -45,7 +52,7 @@ const getHighlightedHandlesForRelatedNode = ( if ( edge.targetHandle !== undefined && edge.targetHandle !== null && - edge.source === activeTableName && + edge.source === targetTableName && edge.target === node.data.table.name ) { handles.push(edge.targetHandle) @@ -55,7 +62,7 @@ const getHighlightedHandlesForRelatedNode = ( edge.sourceHandle !== undefined && edge.sourceHandle !== null && edge.source === node.data.table.name && - edge.target === activeTableName + edge.target === targetTableName ) { handles.push(edge.sourceHandle) } @@ -109,8 +116,12 @@ const unhighlightEdge = (edge: Edge): Edge => ({ export const highlightNodesAndEdges = ( nodes: Node[], edges: Edge[], - activeTableName?: string | undefined, + trigger: { + activeTableName?: string | undefined + hoverTableName?: string | undefined + }, ): { nodes: Node[]; edges: Edge[] } => { + const { activeTableName, hoverTableName } = trigger const edgeMap: EdgeMap = new Map() for (const edge of edges) { const sourceTableName = edge.source @@ -130,9 +141,13 @@ export const highlightNodesAndEdges = ( return activeHighlightNode(node) } - if (isActivelyRelatedNode(activeTableName, edgeMap, node)) { + if ( + isRelatedNodeToTarget(activeTableName, edgeMap, node) || + isHoveredNode(hoverTableName, node) || + isRelatedNodeToTarget(hoverTableName, edgeMap, node) + ) { const highlightedHandles = getHighlightedHandlesForRelatedNode( - activeTableName, + activeTableName ?? hoverTableName, edges, node, ) @@ -143,7 +158,10 @@ export const highlightNodesAndEdges = ( }) const updatedEdges = edges.map((edge) => { - if (isActivelyRelatedEdge(activeTableName, edge)) { + if ( + isRelatedEdgeToTarget(activeTableName, edge) || + isRelatedEdgeToTarget(hoverTableName, edge) + ) { return highlightEdge(edge) } 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 b0e7bfef..d1eaebf8 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/useSyncHighlightsActiveTableChange.ts @@ -21,7 +21,7 @@ export const useSyncHighlightsActiveTableChange = () => { const { nodes: updatedNodes, edges: updatedEdges } = highlightNodesAndEdges( nodes, edges, - tableName, + { activeTableName: tableName }, ) setEdges(updatedEdges)