Skip to content

Commit

Permalink
Merge pull request #309 from liam-hq/fix-active-highlight-node
Browse files Browse the repository at this point in the history
refactor: Refactoring and testing of highlights on active tables
  • Loading branch information
junkisai authored Dec 18, 2024
2 parents 876e4e7 + d8ff5d5 commit 0557af3
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 45 deletions.
6 changes: 6 additions & 0 deletions frontend/.changeset/pink-drinks-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@liam-hq/erd-core": patch
"@liam-hq/cli": patch
---

Refactoring and testing of highlights on active tables
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -99,62 +100,29 @@ export const ERDContentInner: FC<Props> = ({
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,
isHighlighted: true,
highlightedHandles:
highlightedTargetHandles.concat(highlightedSourceHandles) || [],
},
}
}

return {
...node,
data: {
...node.data,
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,
highlightedHandles: [],
isHighlighted: false,
},
}))
const { nodes: updatedNodes } = highlightNodesAndEdges(
nodes,
edges,
undefined,
)

setEdges(updatedEdges)
setNodes(updatedNodes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Node } from '@xyflow/react'

export type Data = {
table: Table
isActiveHighlighted: boolean
isHighlighted: boolean
highlightedHandles: string[]
sourceColumnName: string | undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { aTable } from '@liam-hq/db-structure'
import type { Edge } from '@xyflow/react'
import { describe, expect, it } from 'vitest'
import type { Data, TableNodeType } from './TableNode'
import { highlightNodesAndEdges } from './highlightNodesAndEdges'

const aTableData = (name: string, override?: Partial<Data>): Data => ({
table: aTable({ name }),
isActiveHighlighted: false,
isHighlighted: false,
highlightedHandles: [],
sourceColumnName: undefined,
...override,
})

const aTableNode = (
name: string,
override?: Partial<TableNodeType>,
): 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>,
): 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: [],
}),
}),
])
})
})
Original file line number Diff line number Diff line change
@@ -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<SourceTableName, TargetTableName[]>

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: [] }
}
11 changes: 11 additions & 0 deletions frontend/packages/erd-core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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'),
},
},
})

0 comments on commit 0557af3

Please sign in to comment.