diff --git a/frontend/.changeset/sour-candles-pay.md b/frontend/.changeset/sour-candles-pay.md new file mode 100644 index 00000000..0b8d1e6f --- /dev/null +++ b/frontend/.changeset/sour-candles-pay.md @@ -0,0 +1,6 @@ +--- +"@liam-hq/erd-core": patch +"@liam-hq/cli": patch +--- + +Highlight related edges and cardinalities when a TableNode is active. 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 36f518d4..567d54e5 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx @@ -10,7 +10,7 @@ import { useEdgesState, useNodesState, } from '@xyflow/react' -import { type FC, useCallback } from 'react' +import { type FC, useCallback, useState } from 'react' import styles from './ERDContent.module.css' import { ERDContentProvider, useERDContentContext } from './ERDContentContext' import { RelationshipEdge } from './RelationshipEdge' @@ -65,6 +65,7 @@ export const ERDContentInner: FC = ({ const { state: { loading }, } = useERDContentContext() + const [activeNodeId, setActiveNodeId] = useState(null) useInitialAutoLayout() useActiveTableNameFromUrl() @@ -72,40 +73,58 @@ export const ERDContentInner: FC = ({ enabledFeatures?.fitViewWhenActiveTableChange ?? true, ) - const handleMouseEnterNode: NodeMouseHandler = useCallback( - (_, { id }) => { + const handleNodeClick = useCallback( + (nodeId: string) => { + setActiveNodeId(nodeId) + const relatedEdges = edges.filter( - (e) => e.source === id || e.target === id, + (e) => e.source === nodeId || e.target === nodeId, ) const updatedEdges = edges.map((e) => relatedEdges.includes(e) ? { ...e, animated: true, data: { ...e.data, isHighlighted: true } } - : e, + : { + ...e, + animated: false, + data: { ...e.data, isHighlighted: false }, + }, ) const updatedNodes = nodes.map((node) => { - if (node.id === id) { + if (node.id === nodeId) { return { ...node, data: { ...node.data, isHighlighted: true } } } - const isRelated = isRelatedToTable(relationships, node.id, id) + const isRelated = isRelatedToTable(relationships, node.id, nodeId) + + if (isRelated) { + const highlightedTargetHandles = relatedEdges + .filter((edge) => edge.source === nodeId && edge.target === node.id) + .map((edge) => edge.targetHandle) - const highlightedTargetHandles = relatedEdges - .filter((edge) => edge.source === id && edge.target === node.id) - .map((edge) => edge.targetHandle) + const highlightedSourceHandles = relatedEdges + .filter((edge) => edge.target === nodeId && edge.source === node.id) + .map((edge) => edge.sourceHandle) - const highlightedSourceHandles = relatedEdges - .filter((edge) => edge.target === id && edge.source === node.id) - .map((edge) => edge.sourceHandle) + return { + ...node, + data: { + ...node.data, + isRelated: isRelated, + highlightedHandles: + highlightedTargetHandles.concat(highlightedSourceHandles) || [], + }, + } + } return { ...node, data: { ...node.data, - isRelated: isRelated, - highlightedHandles: - highlightedTargetHandles.concat(highlightedSourceHandles) || [], + isRelated: false, + isHighlighted: false, + highlightedHandles: [], }, } }) @@ -116,20 +135,161 @@ export const ERDContentInner: FC = ({ [edges, nodes, setNodes, setEdges, relationships], ) - const handleMouseLeaveNode: NodeMouseHandler = useCallback( + const handlePaneClick = useCallback(() => { + setActiveNodeId(null) + + const updatedEdges = edges.map((e) => ({ + ...e, + animated: false, + data: { ...e.data, isHighlighted: false }, + })) + + const updatedNodes = nodes.map((node) => ({ + ...node, + data: { + ...node.data, + isRelated: false, + highlightedHandles: [], + isHighlighted: false, + }, + })) + + setEdges(updatedEdges) + setNodes(updatedNodes) + }, [edges, nodes, setNodes, setEdges]) + + const handleMouseEnterNode: NodeMouseHandler = useCallback( (_, { id }) => { + const relatedEdges = edges.filter( + (e) => e.source === id || e.target === id, + ) + const updatedEdges = edges.map((e) => - e.source === id || e.target === id - ? { - ...e, - animated: false, - data: { ...e.data, isHighlighted: false }, - } + relatedEdges.includes(e) + ? { ...e, animated: true, data: { ...e.data, isHighlighted: true } } : e, ) const updatedNodes = nodes.map((node) => { - return { + if (node.id === id) { + return { ...node, data: { ...node.data, isHighlighted: true } } + } + + const isRelated = isRelatedToTable(relationships, node.id, id) + + if (isRelated) { + const highlightedTargetHandles = relatedEdges + .filter((edge) => edge.source === id && edge.target === node.id) + .map((edge) => edge.targetHandle) + + const highlightedSourceHandles = relatedEdges + .filter((edge) => edge.target === id && edge.source === node.id) + .map((edge) => edge.sourceHandle) + + return { + ...node, + data: { + ...node.data, + isRelated: isRelated, + highlightedHandles: + highlightedTargetHandles.concat(highlightedSourceHandles) || [], + }, + } + } + + return node + }) + + setEdges(updatedEdges) + setNodes(updatedNodes) + }, + [edges, nodes, setNodes, setEdges, relationships], + ) + + 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) + ? { + ...e, + animated: true, + data: { ...e.data, isHighlighted: true }, + } + : { + ...e, + animated: false, + data: { ...e.data, isHighlighted: false }, + }, + ) + setEdges(updatedEdges) + + const updatedNodes = nodes.map((node) => { + const isRelated = isRelatedToTable( + relationships, + node.id, + activeNodeId, + ) + + if (node.id === activeNodeId || isRelated) { + const highlightedTargetHandles = relatedEdges + .filter( + (edge) => + edge.source === activeNodeId && edge.target === node.id, + ) + .map((edge) => edge.targetHandle) + + const highlightedSourceHandles = relatedEdges + .filter( + (edge) => + edge.target === activeNodeId && edge.source === node.id, + ) + .map((edge) => edge.sourceHandle) + + const isHighlighted = node.id === activeNodeId + + return { + ...node, + data: { + ...node.data, + isRelated: isRelated, + isHighlighted: isHighlighted, + highlightedHandles: + highlightedTargetHandles.concat(highlightedSourceHandles) || + [], + }, + } + } + + return { + ...node, + data: { + ...node.data, + isRelated: false, + isHighlighted: false, + highlightedHandles: [], + }, + } + }) + + setNodes(updatedNodes) + } else { + const updatedEdges = edges.map((e) => + e.source === id || e.target === id + ? { + ...e, + animated: false, + data: { ...e.data, isHighlighted: false }, + } + : e, + ) + setEdges(updatedEdges) + + const updatedNodes = nodes.map((node) => ({ ...node, data: { ...node.data, @@ -137,13 +297,11 @@ export const ERDContentInner: FC = ({ highlightedHandles: [], isHighlighted: false, }, - } - }) - - setEdges(updatedEdges) - setNodes(updatedNodes) + })) + setNodes(updatedNodes) + } }, - [edges, nodes, setNodes, setEdges], + [edges, nodes, setNodes, setEdges, activeNodeId, relationships], ) const panOnDrag = [1, 2] @@ -151,7 +309,13 @@ export const ERDContentInner: FC = ({ return (
({ + ...node, + data: { + ...node.data, + onClick: handleNodeClick, + }, + }))} edges={edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} @@ -159,6 +323,8 @@ export const ERDContentInner: FC = ({ maxZoom={2} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + onNodeClick={(_, node) => handleNodeClick(node.id)} + onPaneClick={handlePaneClick} onNodeMouseEnter={handleMouseEnterNode} onNodeMouseLeave={handleMouseLeaveNode} panOnScroll