diff --git a/frontend/.changeset/forty-berries-hammer.md b/frontend/.changeset/forty-berries-hammer.md new file mode 100644 index 00000000..06866b0f --- /dev/null +++ b/frontend/.changeset/forty-berries-hammer.md @@ -0,0 +1,6 @@ +--- +"@liam-hq/erd-core": patch +"@liam-hq/cli": patch +--- + +Resolving the issue of remaining highlights diff --git a/frontend/.changeset/pink-drinks-applaud.md b/frontend/.changeset/pink-drinks-applaud.md new file mode 100644 index 00000000..46906268 --- /dev/null +++ b/frontend/.changeset/pink-drinks-applaud.md @@ -0,0 +1,6 @@ +--- +"@liam-hq/erd-core": patch +"@liam-hq/cli": patch +--- + +Refactoring and testing of highlights on active tables diff --git a/frontend/.changeset/tricky-comics-raise.md b/frontend/.changeset/tricky-comics-raise.md new file mode 100644 index 00000000..dbdf5e0f --- /dev/null +++ b/frontend/.changeset/tricky-comics-raise.md @@ -0,0 +1,6 @@ +--- +"@liam-hq/erd-core": patch +"@liam-hq/cli": patch +--- + +refactor Integrated `isRelated` into `isHighlighted` 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 8de172f3..f14f073e 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/ERDContent.tsx @@ -16,6 +16,7 @@ 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 { useUpdateNodeCardinalities } from './useUpdateNodeCardinalities' @@ -67,6 +68,22 @@ 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, @@ -99,64 +116,29 @@ export const ERDContentInner: FC = ({ relatedEdges.includes(e) ? highlightEdge(e) : unhighlightEdge(e), ) - const updatedNodes = nodes.map((node) => { - if (node.id === nodeId) { - return { ...node, data: { ...node.data, isHighlighted: true } } - } - - 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 highlightedSourceHandles = relatedEdges - .filter((edge) => edge.target === nodeId && 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: false, - isHighlighted: false, - highlightedHandles: [], - }, - } - }) + const { nodes: updatedNodes } = highlightNodesAndEdges( + nodes, + edges, + nodeId, + ) setEdges(updatedEdges) setNodes(updatedNodes) }, - [edges, nodes, setNodes, setEdges, relationships], + [edges, nodes, setNodes, setEdges], ) const handlePaneClick = useCallback(() => { setActiveNodeId(null) + updateActiveTableName(undefined) const updatedEdges = edges.map(unhighlightEdge) - const updatedNodes = nodes.map((node) => ({ - ...node, - data: { - ...node.data, - isRelated: false, - highlightedHandles: [], - isHighlighted: false, - }, - })) + const { nodes: updatedNodes } = highlightNodesAndEdges( + nodes, + edges, + undefined, + ) setEdges(updatedEdges) setNodes(updatedNodes) @@ -168,44 +150,79 @@ export const ERDContentInner: FC = ({ (e) => e.source === id || e.target === id, ) - const updatedEdges = edges.map((e) => - relatedEdges.includes(e) ? highlightEdge(e) : e, + 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) { - const highlightedTargetHandles = relatedEdges - .filter((edge) => edge.source === id && edge.target === node.id) - .map((edge) => edge.targetHandle) + let highlightedHandles = getHighlightedHandles(edges, id, node.id) + if (isRelatedToActiveNode) { + highlightedHandles = highlightedHandles.concat( + getHighlightedHandles(edges, activeNodeId, node.id), + ) + } - const highlightedSourceHandles = relatedEdges - .filter((edge) => edge.target === id && edge.source === node.id) - .map((edge) => edge.sourceHandle) + 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, - isRelated: isRelated, - highlightedHandles: - highlightedTargetHandles.concat(highlightedSourceHandles) || [], + isHighlighted: isHighlighted, + highlightedHandles: highlightedHandles, }, } } - return node + return { + ...node, + data: { + ...node.data, + isHighlighted: false, + highlightedHandles: [], + }, + } }) setEdges(updatedEdges) setNodes(updatedNodes) }, - [edges, nodes, setNodes, setEdges, relationships], + [edges, nodes, setNodes, setEdges, relationships, activeNodeId], ) const handleMouseLeaveNode: NodeMouseHandler = useCallback( @@ -228,19 +245,11 @@ export const ERDContentInner: FC = ({ ) 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 highlightedHandles = getHighlightedHandles( + edges, + activeNodeId, + node.id, + ) const isHighlighted = node.id === activeNodeId @@ -248,11 +257,8 @@ export const ERDContentInner: FC = ({ ...node, data: { ...node.data, - isRelated: isRelated, - isHighlighted: isHighlighted, - highlightedHandles: - highlightedTargetHandles.concat(highlightedSourceHandles) || - [], + isHighlighted, + highlightedHandles: highlightedHandles, }, } } @@ -261,7 +267,6 @@ export const ERDContentInner: FC = ({ ...node, data: { ...node.data, - isRelated: false, isHighlighted: false, highlightedHandles: [], }, @@ -279,7 +284,6 @@ export const ERDContentInner: FC = ({ ...node, data: { ...node.data, - isRelated: false, highlightedHandles: [], isHighlighted: false, }, diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.module.css b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.module.css index 1ae1a2fa..ec87d7c0 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.module.css +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.module.css @@ -12,7 +12,7 @@ opacity: 0; } -.wrapperHover, +.wrapperHighlighted, .wrapper:hover { border: 1px solid var(--primary-accent); box-shadow: 0px 0px 20px 0px rgba(29, 237, 131, 0.4); diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.tsx b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.tsx index 28ce8079..e952b145 100644 --- a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.tsx +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/TableNode/TableNode.tsx @@ -22,15 +22,15 @@ export const TableNode: FC = ({ data }) => { const isActive = tableName === data.table.name - const isTableRelated = - data.isRelated || + const isTableHighlighted = + data.isHighlighted || isRelatedToTable(relationships, data.table.name, tableName) return (
): Data => ({ + table: aTable({ name }), + isActiveHighlighted: false, + isHighlighted: false, + highlightedHandles: [], + sourceColumnName: undefined, + ...override, +}) + +const aTableNode = ( + name: string, + override?: Partial, +): TableNodeType => ({ + id: name, + type: 'table', + position: { x: 0, y: 0 }, + ...override, + data: aTableData(name, override?.data), +}) + +const anEdge = ( + source: string, + target: string, + sourceHandle: string | null, + targetHandle: string | null, + override?: Partial, +): Edge => ({ + id: `${source}-${target}`, + source, + sourceHandle, + target, + targetHandle, + animated: false, + data: { isHighlighted: false, ...override?.data }, + ...override, +}) + +describe(highlightNodesAndEdges, () => { + const nodes: TableNodeType[] = [ + aTableNode('users'), + aTableNode('posts'), + aTableNode('comments'), + aTableNode('comment_users'), + ] + + const edges: Edge[] = [ + anEdge('users', 'posts', 'users-id', 'posts-user_id'), + anEdge('users', 'comment_users', 'users-id', 'comment_users-user_id'), + anEdge( + 'comments', + 'comment_users', + 'comments-id', + 'comment_users-comment_id', + ), + ] + + it('When the users is active, the users and related tables are highlighted', () => { + const { nodes: updatedNodes } = highlightNodesAndEdges( + nodes, + edges, + 'users', + ) + + expect(updatedNodes).toEqual([ + aTableNode('users', { + data: aTableData('users', { isActiveHighlighted: 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 no active table, no tables are highlighted', () => { + const { nodes: updatedNodes } = highlightNodesAndEdges( + nodes, + edges, + undefined, + ) + + expect(updatedNodes).toEqual([ + aTableNode('users', { + data: aTableData('users', { isActiveHighlighted: false }), + }), + aTableNode('posts', { + data: aTableData('posts', { + isHighlighted: false, + highlightedHandles: [], + }), + }), + aTableNode('comments'), + aTableNode('comment_users', { + data: aTableData('comment_users', { + isHighlighted: false, + highlightedHandles: [], + }), + }), + ]) + }) +}) diff --git a/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts new file mode 100644 index 00000000..cc56e2ac --- /dev/null +++ b/frontend/packages/erd-core/src/components/ERDRenderer/ERDContent/highlightNodesAndEdges.ts @@ -0,0 +1,127 @@ +import type { Edge, Node } from '@xyflow/react' +import { type TableNodeType, isTableNode } from './TableNode' + +type SourceTableName = string +type TargetTableName = string +type EdgeMap = Map + +const isActiveNode = ( + activeTableName: string | undefined, + node: TableNodeType, +): boolean => { + return node.data.table.name === activeTableName +} + +const isActivelyRelatedNode = ( + activeTableName: string | undefined, + edgeMap: EdgeMap, + node: TableNodeType, +): boolean => { + if (!activeTableName) { + return false + } + + return edgeMap.get(activeTableName)?.includes(node.data.table.name) ?? false +} + +const getHighlightedHandlesForRelatedNode = ( + activeTableName: string | undefined, + edges: Edge[], + node: TableNodeType, +): string[] => { + if (!activeTableName) { + return [] + } + + const handles: string[] = [] + for (const edge of edges) { + if ( + edge.targetHandle !== undefined && + edge.targetHandle !== null && + edge.source === activeTableName && + edge.target === node.data.table.name + ) { + handles.push(edge.targetHandle) + } + + if ( + edge.sourceHandle !== undefined && + edge.sourceHandle !== null && + edge.source === node.data.table.name && + edge.target === activeTableName + ) { + handles.push(edge.sourceHandle) + } + } + + return handles +} + +const activeHighlightNode = (node: TableNodeType): TableNodeType => ({ + ...node, + data: { + ...node.data, + isActiveHighlighted: true, + }, +}) + +const highlightNode = ( + node: TableNodeType, + handles: string[], +): TableNodeType => ({ + ...node, + data: { + ...node.data, + isHighlighted: true, + highlightedHandles: handles, + }, +}) + +const unhighlightNode = (node: TableNodeType): TableNodeType => ({ + ...node, + data: { + ...node.data, + isActiveHighlighted: false, + isHighlighted: false, + highlightedHandles: [], + }, +}) + +export const highlightNodesAndEdges = ( + nodes: Node[], + edges: Edge[], + activeTableName?: string | undefined, +): { nodes: Node[]; edges: Edge[] } => { + const edgeMap: EdgeMap = new Map() + for (const edge of edges) { + const sourceTableName = edge.source + const targetTableName = edge.target + if (!edgeMap.has(sourceTableName)) { + edgeMap.set(sourceTableName, []) + } + edgeMap.get(sourceTableName)?.push(targetTableName) + } + + const updatedNodes = nodes.map((node) => { + if (!isTableNode(node)) { + return node + } + + if (isActiveNode(activeTableName, node)) { + return activeHighlightNode(node) + } + + if (isActivelyRelatedNode(activeTableName, edgeMap, node)) { + const highlightedHandles = getHighlightedHandlesForRelatedNode( + activeTableName, + edges, + node, + ) + return highlightNode(node, highlightedHandles) + } + + return unhighlightNode(node) + }) + + return { nodes: updatedNodes, edges: [] } +} diff --git a/frontend/packages/erd-core/vitest.config.ts b/frontend/packages/erd-core/vitest.config.ts new file mode 100644 index 00000000..eb9cc4a3 --- /dev/null +++ b/frontend/packages/erd-core/vitest.config.ts @@ -0,0 +1,11 @@ +// biome-ignore lint/correctness/noNodejsModules: Because this file is a config file +import * as path from 'node:path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})