fix: general performance improvements on canvas (#751)

This commit is contained in:
Guy Ben-Aharon
2025-07-04 12:23:29 +03:00
committed by GitHub
parent d15985e399
commit 4fcc49d49a
4 changed files with 557 additions and 361 deletions
+72 -40
View File
@@ -84,6 +84,9 @@ import type { AreaNodeType } from './area-node/area-node';
import { AreaNode } from './area-node/area-node';
import type { Area } from '@/lib/domain/area';
const HIGHLIGHTED_EDGE_Z_INDEX = 1;
const DEFAULT_EDGE_Z_INDEX = 0;
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
export type NodeType = TableNodeType | AreaNodeType;
@@ -132,7 +135,7 @@ export interface CanvasProps {
}
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
const { getEdge, getInternalNode, getNode } = useReactFlow();
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
string[]
@@ -282,22 +285,36 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
}, [edges, setSelectedRelationshipIds, selectedRelationshipIds]);
useEffect(() => {
const tablesSelectedEdges = getEdges()
.filter(
(edge) =>
selectedTableIds.includes(edge.source) ||
selectedTableIds.includes(edge.target)
)
.map((edge) => edge.id);
const selectedTableIdsSet = new Set(selectedTableIds);
const selectedRelationshipIdsSet = new Set(selectedRelationshipIds);
const allSelectedEdges = [
...tablesSelectedEdges,
...selectedRelationshipIds,
];
setEdges((prevEdges) => {
// Check if any edge needs updating
let hasChanges = false;
setEdges((edges) =>
edges.map((edge): EdgeType => {
const selected = allSelectedEdges.includes(edge.id);
const newEdges = prevEdges.map((edge): EdgeType => {
const shouldBeHighlighted =
selectedRelationshipIdsSet.has(edge.id) ||
selectedTableIdsSet.has(edge.source) ||
selectedTableIdsSet.has(edge.target);
const currentHighlighted = edge.data?.highlighted ?? false;
const currentAnimated = edge.animated ?? false;
const currentZIndex = edge.zIndex ?? 0;
// Skip if no changes needed
if (
currentHighlighted === shouldBeHighlighted &&
currentAnimated === shouldBeHighlighted &&
currentZIndex ===
(shouldBeHighlighted
? HIGHLIGHTED_EDGE_Z_INDEX
: DEFAULT_EDGE_Z_INDEX)
) {
return edge;
}
hasChanges = true;
if (edge.type === 'dependency-edge') {
const dependencyEdge = edge as DependencyEdgeType;
@@ -305,10 +322,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
...dependencyEdge,
data: {
...dependencyEdge.data!,
highlighted: selected,
highlighted: shouldBeHighlighted,
},
animated: selected,
zIndex: selected ? 1 : 0,
animated: shouldBeHighlighted,
zIndex: shouldBeHighlighted
? HIGHLIGHTED_EDGE_Z_INDEX
: DEFAULT_EDGE_Z_INDEX,
};
} else {
const relationshipEdge = edge as RelationshipEdgeType;
@@ -316,34 +335,47 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
...relationshipEdge,
data: {
...relationshipEdge.data!,
highlighted: selected,
highlighted: shouldBeHighlighted,
},
animated: selected,
zIndex: selected ? 1 : 0,
animated: shouldBeHighlighted,
zIndex: shouldBeHighlighted
? HIGHLIGHTED_EDGE_Z_INDEX
: DEFAULT_EDGE_Z_INDEX,
};
}
})
);
}, [selectedRelationshipIds, selectedTableIds, setEdges, getEdges]);
});
return hasChanges ? newEdges : prevEdges;
});
}, [selectedRelationshipIds, selectedTableIds, setEdges]);
useEffect(() => {
setNodes([
...tables.map((table) => {
const isOverlapping =
(overlapGraph.graph.get(table.id) ?? []).length > 0;
const node = tableToTableNode(table, filteredSchemas);
setNodes((prevNodes) => {
const newNodes = [
...tables.map((table) => {
const isOverlapping =
(overlapGraph.graph.get(table.id) ?? []).length > 0;
const node = tableToTableNode(table, filteredSchemas);
return {
...node,
data: {
...node.data,
isOverlapping,
highlightOverlappingTables,
},
};
}),
...areas.map(areaToAreaNode),
]);
return {
...node,
data: {
...node.data,
isOverlapping,
highlightOverlappingTables,
},
};
}),
...areas.map(areaToAreaNode),
];
// Check if nodes actually changed
if (equal(prevNodes, newNodes)) {
return prevNodes;
}
return newNodes;
});
}, [
tables,
areas,
@@ -17,203 +17,234 @@ export type RelationshipEdgeType = Edge<
'relationship-edge'
>;
export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
source,
target,
selected,
data,
}) => {
const { getInternalNode, getEdge } = useReactFlow();
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
const { relationships } = useChartDB();
const relationship = data?.relationship;
const openRelationshipInEditor = useCallback(() => {
selectSidebarSection('relationships');
openRelationshipFromSidebar(id);
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
const edgeNumber = useMemo(
() =>
relationships
.filter(
(relationship) =>
(relationship.targetTableId === target &&
relationship.sourceTableId === source) ||
(relationship.targetTableId === source &&
relationship.sourceTableId === target)
)
.findIndex((relationship) => relationship.id === id),
[relationships, id, source, target]
);
const sourceNode = getInternalNode(source);
const targetNode = getInternalNode(target);
const edge = getEdge(id);
const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.(
RIGHT_HANDLE_ID_PREFIX
)
? 'right'
: 'left';
const sourceWidth = sourceNode?.measured.width ?? 0;
const sourceLeftX =
sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10;
const sourceRightX =
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
const targetWidth = targetNode?.measured.width ?? 0;
const targetLeftX = targetX - 1;
const targetRightX = targetX + targetWidth + 10;
const { sourceSide, targetSide } = useMemo(() => {
const distances = {
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
leftToRight: Math.abs(sourceLeftX - targetRightX),
rightToLeft: Math.abs(sourceRightX - targetLeftX),
rightToRight: Math.abs(sourceRightX - targetRightX),
};
const minDistance = Math.min(
distances.leftToLeft,
distances.leftToRight,
distances.rightToLeft,
distances.rightToRight
);
const minDistanceKey = Object.keys(distances).find(
(key) => distances[key as keyof typeof distances] === minDistance
) as keyof typeof distances;
switch (minDistanceKey) {
case 'leftToRight':
return { sourceSide: 'left', targetSide: 'right' };
case 'rightToLeft':
return { sourceSide: 'right', targetSide: 'left' };
case 'rightToRight':
return { sourceSide: 'right', targetSide: 'right' };
default:
return { sourceSide: 'left', targetSide: 'left' };
}
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
const [edgePath] = useMemo(
() =>
getSmoothStepPath({
sourceX: sourceSide === 'left' ? sourceLeftX : sourceRightX,
sourceY,
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
targetY,
borderRadius: 14,
sourcePosition:
sourceSide === 'left' ? Position.Left : Position.Right,
targetPosition:
targetSide === 'left' ? Position.Left : Position.Right,
offset: (edgeNumber + 1) * 14,
}),
[
sourceSide,
targetSide,
sourceLeftX,
sourceRightX,
targetLeftX,
targetRightX,
export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> =
React.memo(
({
id,
sourceX,
sourceY,
targetX,
targetY,
edgeNumber,
]
);
source,
target,
selected,
data,
}) => {
const { getInternalNode, getEdge } = useReactFlow();
const { openRelationshipFromSidebar, selectSidebarSection } =
useLayout();
const { checkIfRelationshipRemoved, checkIfNewRelationship } =
useDiff();
const sourceMarker = useMemo(
() =>
getCardinalityMarkerId({
cardinality: relationship?.sourceCardinality ?? 'one',
selected: selected ?? false,
side: sourceSide as 'left' | 'right',
}),
[relationship?.sourceCardinality, selected, sourceSide]
);
const targetMarker = useMemo(
() =>
getCardinalityMarkerId({
cardinality: relationship?.targetCardinality ?? 'one',
selected: selected ?? false,
side: targetSide as 'left' | 'right',
}),
[relationship?.targetCardinality, selected, targetSide]
);
const { relationships } = useChartDB();
const isDiffNewRelationship = useMemo(
() =>
relationship?.id
? checkIfNewRelationship({ relationshipId: relationship.id })
: false,
[checkIfNewRelationship, relationship?.id]
);
const relationship = data?.relationship;
const isDiffRelationshipRemoved = useMemo(
() =>
relationship?.id
? checkIfRelationshipRemoved({
relationshipId: relationship.id,
})
: false,
[checkIfRelationshipRemoved, relationship?.id]
);
const openRelationshipInEditor = useCallback(() => {
selectSidebarSection('relationships');
openRelationshipFromSidebar(id);
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
return (
<>
<path
id={id}
d={edgePath}
markerStart={`url(#${sourceMarker})`}
markerEnd={`url(#${targetMarker})`}
fill="none"
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
{
'!stroke-green-500 !stroke-[3px]':
isDiffNewRelationship,
'!stroke-red-500 !stroke-[3px]':
isDiffRelationshipRemoved,
},
])}
onClick={(e) => {
if (e.detail === 2) {
openRelationshipInEditor();
const edgeNumber = useMemo(() => {
let index = 0;
for (const rel of relationships) {
if (
(rel.targetTableId === target &&
rel.sourceTableId === source) ||
(rel.targetTableId === source &&
rel.sourceTableId === target)
) {
if (rel.id === id) return index;
index++;
}
}}
/>
<path
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="react-flow__edge-interaction"
onClick={(e) => {
if (e.detail === 2) {
openRelationshipInEditor();
}
}}
/>
</>
// <BaseEdge
// id={id}
// path={edgePath}
// markerStart="url(#cardinality_one)"
// markerEnd="url(#cardinality_one)"
// className={`!stroke-2 ${selected ? '!stroke-slate-500' : '!stroke-slate-300'}`}
// />
}
return -1;
}, [relationships, id, source, target]);
const sourceNode = useMemo(
() => getInternalNode(source),
[getInternalNode, source]
);
const targetNode = useMemo(
() => getInternalNode(target),
[getInternalNode, target]
);
const edge = useMemo(() => getEdge(id), [getEdge, id]);
const sourceHandle: 'left' | 'right' = useMemo(
() =>
edge?.sourceHandle?.startsWith?.(RIGHT_HANDLE_ID_PREFIX)
? 'right'
: 'left',
[edge?.sourceHandle]
);
const sourceWidth = sourceNode?.measured.width ?? 0;
const sourceLeftX =
sourceHandle === 'left'
? sourceX + 3
: sourceX - sourceWidth - 10;
const sourceRightX =
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
const targetWidth = targetNode?.measured.width ?? 0;
const targetLeftX = targetX - 1;
const targetRightX = targetX + targetWidth + 10;
const { sourceSide, targetSide } = useMemo(() => {
const distances = {
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
leftToRight: Math.abs(sourceLeftX - targetRightX),
rightToLeft: Math.abs(sourceRightX - targetLeftX),
rightToRight: Math.abs(sourceRightX - targetRightX),
};
const minDistance = Math.min(
distances.leftToLeft,
distances.leftToRight,
distances.rightToLeft,
distances.rightToRight
);
const minDistanceKey = Object.keys(distances).find(
(key) =>
distances[key as keyof typeof distances] === minDistance
) as keyof typeof distances;
switch (minDistanceKey) {
case 'leftToRight':
return { sourceSide: 'left', targetSide: 'right' };
case 'rightToLeft':
return { sourceSide: 'right', targetSide: 'left' };
case 'rightToRight':
return { sourceSide: 'right', targetSide: 'right' };
default:
return { sourceSide: 'left', targetSide: 'left' };
}
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
const edgePath = useMemo(() => {
// Round values to prevent tiny changes from triggering recalculation
const roundedSourceX = Math.round(
sourceSide === 'left' ? sourceLeftX : sourceRightX
);
const roundedTargetX = Math.round(
targetSide === 'left' ? targetLeftX : targetRightX
);
const roundedSourceY = Math.round(sourceY);
const roundedTargetY = Math.round(targetY);
const [path] = getSmoothStepPath({
sourceX: roundedSourceX,
sourceY: roundedSourceY,
targetX: roundedTargetX,
targetY: roundedTargetY,
borderRadius: 14,
sourcePosition:
sourceSide === 'left' ? Position.Left : Position.Right,
targetPosition:
targetSide === 'left' ? Position.Left : Position.Right,
offset: (edgeNumber + 1) * 14,
});
return path;
}, [
sourceLeftX,
sourceRightX,
targetLeftX,
targetRightX,
sourceY,
targetY,
sourceSide,
targetSide,
edgeNumber,
]);
const sourceMarker = useMemo(
() =>
getCardinalityMarkerId({
cardinality: relationship?.sourceCardinality ?? 'one',
selected: selected ?? false,
side: sourceSide as 'left' | 'right',
}),
[relationship?.sourceCardinality, selected, sourceSide]
);
const targetMarker = useMemo(
() =>
getCardinalityMarkerId({
cardinality: relationship?.targetCardinality ?? 'one',
selected: selected ?? false,
side: targetSide as 'left' | 'right',
}),
[relationship?.targetCardinality, selected, targetSide]
);
const isDiffNewRelationship = useMemo(
() =>
relationship?.id
? checkIfNewRelationship({
relationshipId: relationship.id,
})
: false,
[checkIfNewRelationship, relationship?.id]
);
const isDiffRelationshipRemoved = useMemo(
() =>
relationship?.id
? checkIfRelationshipRemoved({
relationshipId: relationship.id,
})
: false,
[checkIfRelationshipRemoved, relationship?.id]
);
return (
<>
<path
id={id}
d={edgePath}
markerStart={`url(#${sourceMarker})`}
markerEnd={`url(#${targetMarker})`}
fill="none"
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
{
'!stroke-green-500 !stroke-[3px]':
isDiffNewRelationship,
'!stroke-red-500 !stroke-[3px]':
isDiffRelationshipRemoved,
},
])}
onClick={(e) => {
if (e.detail === 2) {
openRelationshipInEditor();
}
}}
/>
<path
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="react-flow__edge-interaction"
onClick={(e) => {
if (e.detail === 2) {
openRelationshipInEditor();
}
}}
/>
</>
// <BaseEdge
// id={id}
// path={edgePath}
// markerStart="url(#cardinality_one)"
// markerEnd="url(#cardinality_one)"
// className={`!stroke-2 ${selected ? '!stroke-slate-500' : '!stroke-slate-300'}`}
// />
);
}
);
};
RelationshipEdge.displayName = 'RelationshipEdge';
@@ -46,6 +46,27 @@ export interface TableNodeFieldProps {
isConnectable: boolean;
}
const arePropsEqual = (
prevProps: TableNodeFieldProps,
nextProps: TableNodeFieldProps
) => {
return (
prevProps.field.id === nextProps.field.id &&
prevProps.field.name === nextProps.field.name &&
prevProps.field.primaryKey === nextProps.field.primaryKey &&
prevProps.field.nullable === nextProps.field.nullable &&
prevProps.field.comments === nextProps.field.comments &&
prevProps.field.unique === nextProps.field.unique &&
prevProps.field.type.id === nextProps.field.type.id &&
prevProps.field.type.name === nextProps.field.type.name &&
prevProps.focused === nextProps.focused &&
prevProps.highlighted === nextProps.highlighted &&
prevProps.visible === nextProps.visible &&
prevProps.isConnectable === nextProps.isConnectable &&
prevProps.tableNodeId === nextProps.tableNodeId
);
};
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
const { removeField, relationships, readonly, updateField } =
@@ -64,17 +85,25 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
connection.fromHandle.id?.startsWith(
LEFT_HANDLE_ID_PREFIX
)),
[connection, tableNodeId]
);
const numberOfEdgesToField = useMemo(
() =>
relationships.filter(
(relationship) =>
relationship.targetTableId === tableNodeId &&
relationship.targetFieldId === field.id
).length,
[relationships, tableNodeId, field.id]
[
connection.inProgress,
connection.fromNode?.id,
connection.fromHandle?.id,
tableNodeId,
]
);
const numberOfEdgesToField = useMemo(() => {
let count = 0;
for (const rel of relationships) {
if (
rel.targetTableId === tableNodeId &&
rel.targetFieldId === field.id
) {
count++;
}
}
return count;
}, [relationships, tableNodeId, field.id]);
const previousNumberOfEdgesToFieldRef = useRef(numberOfEdgesToField);
@@ -82,8 +111,12 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
if (
previousNumberOfEdgesToFieldRef.current !== numberOfEdgesToField
) {
updateNodeInternals(tableNodeId);
previousNumberOfEdgesToFieldRef.current = numberOfEdgesToField;
const timer = setTimeout(() => {
updateNodeInternals(tableNodeId);
previousNumberOfEdgesToFieldRef.current =
numberOfEdgesToField;
}, 100);
return () => clearTimeout(timer);
}
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
@@ -112,39 +145,63 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
checkIfFieldHasChange,
} = useDiff();
const isDiffFieldRemoved = useMemo(
() => checkIfFieldRemoved({ fieldId: field.id }),
[checkIfFieldRemoved, field.id]
);
const [diffState, setDiffState] = useState<{
isDiffFieldRemoved: boolean;
isDiffNewField: boolean;
fieldDiffChangedName: string | null;
fieldDiffChangedType: DBField['type'] | null;
isDiffFieldChanged: boolean;
}>({
isDiffFieldRemoved: false,
isDiffNewField: false,
fieldDiffChangedName: null,
fieldDiffChangedType: null,
isDiffFieldChanged: false,
});
const isDiffNewField = useMemo(
() => checkIfNewField({ fieldId: field.id }),
[checkIfNewField, field.id]
);
useEffect(() => {
// Calculate diff state asynchronously
const timer = requestAnimationFrame(() => {
setDiffState({
isDiffFieldRemoved: checkIfFieldRemoved({
fieldId: field.id,
}),
isDiffNewField: checkIfNewField({ fieldId: field.id }),
fieldDiffChangedName: getFieldNewName({
fieldId: field.id,
}),
fieldDiffChangedType: getFieldNewType({
fieldId: field.id,
}),
isDiffFieldChanged: checkIfFieldHasChange({
fieldId: field.id,
tableId: tableNodeId,
}),
});
});
return () => cancelAnimationFrame(timer);
}, [
checkIfFieldRemoved,
checkIfNewField,
getFieldNewName,
getFieldNewType,
checkIfFieldHasChange,
field.id,
tableNodeId,
]);
const fieldDiffChangedName = useMemo(
() => getFieldNewName({ fieldId: field.id }),
[getFieldNewName, field.id]
);
const {
isDiffFieldRemoved,
isDiffNewField,
fieldDiffChangedName,
fieldDiffChangedType,
isDiffFieldChanged,
} = diffState;
const fieldDiffChangedType = useMemo(
() => getFieldNewType({ fieldId: field.id }),
[getFieldNewType, field.id]
);
const isDiffFieldChanged = useMemo(
() =>
checkIfFieldHasChange({
fieldId: field.id,
tableId: tableNodeId,
}),
[checkIfFieldHasChange, field.id, tableNodeId]
);
const enterEditMode = (e: React.MouseEvent) => {
const enterEditMode = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
}, []);
return (
<div
@@ -359,7 +416,8 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
)}
</div>
);
}
},
arePropsEqual
);
TableNodeField.displayName = 'TableNodeField';
@@ -1,4 +1,10 @@
import React, { useCallback, useState, useMemo } from 'react';
import React, {
useCallback,
useState,
useMemo,
useRef,
useEffect,
} from 'react';
import type { NodeProps, Node } from '@xyflow/react';
import { NodeResizer, useStore } from '@xyflow/react';
import { Button } from '@/components/button/button';
@@ -95,34 +101,89 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
return table.color;
}, [tableChangedColor, table.color]);
const isDiffTableChanged = useMemo(
() => checkIfTableHasChange({ tableId: table.id }),
[checkIfTableHasChange, table.id]
const [diffState, setDiffState] = useState<{
isDiffTableChanged: boolean;
isDiffNewTable: boolean;
isDiffTableRemoved: boolean;
}>({
isDiffTableChanged: false,
isDiffNewTable: false,
isDiffTableRemoved: false,
});
const hasMountedRef = useRef(false);
useEffect(() => {
// Skip on initial mount to improve performance
const calculateDiff = () => {
setDiffState({
isDiffTableChanged: checkIfTableHasChange({
tableId: table.id,
}),
isDiffNewTable: checkIfNewTable({ tableId: table.id }),
isDiffTableRemoved: checkIfTableRemoved({
tableId: table.id,
}),
});
};
if (!hasMountedRef.current) {
hasMountedRef.current = true;
// Defer diff calculation
requestAnimationFrame(calculateDiff);
} else {
calculateDiff();
}
}, [
checkIfTableHasChange,
checkIfNewTable,
checkIfTableRemoved,
table.id,
]);
const { isDiffTableChanged, isDiffNewTable, isDiffTableRemoved } =
diffState;
const selectedRelEdges: RelationshipEdgeType[] = useMemo(() => {
if (edges.length === 0) return [];
const relEdges: RelationshipEdgeType[] = [];
for (const edge of edges) {
if (
edge.type === 'relationship-edge' &&
(edge.source === id || edge.target === id) &&
(edge.selected || edge.data?.highlighted)
) {
relEdges.push(edge as RelationshipEdgeType);
}
}
return relEdges;
}, [edges, id]);
const highlightedFieldIds = useMemo(() => {
const fieldIds = new Set<string>();
selectedRelEdges.forEach((edge) => {
if (edge.data?.relationship.sourceFieldId) {
fieldIds.add(edge.data.relationship.sourceFieldId);
}
if (edge.data?.relationship.targetFieldId) {
fieldIds.add(edge.data.relationship.targetFieldId);
}
});
return fieldIds;
}, [selectedRelEdges]);
const focused = useMemo(
() => (!!selected && !dragging) || isHovering,
[selected, dragging, isHovering]
);
const isDiffNewTable = useMemo(
() => checkIfNewTable({ tableId: table.id }),
[checkIfNewTable, table.id]
);
const isDiffTableRemoved = useMemo(
() => checkIfTableRemoved({ tableId: table.id }),
[checkIfTableRemoved, table.id]
);
const selectedRelEdges = edges.filter(
(edge) =>
(edge.source === id || edge.target === id) &&
(edge.selected || edge.data?.highlighted) &&
edge.type === 'relationship-edge'
) as RelationshipEdgeType[];
const focused = (!!selected && !dragging) || isHovering;
const openTableInEditor = () => {
const openTableInEditor = useCallback(() => {
selectSidebarSection('tables');
openTableFromSidebar(table.id);
};
}, [selectSidebarSection, openTableFromSidebar, table.id]);
const expandTable = useCallback(() => {
updateTable(table.id, {
@@ -147,47 +208,56 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
});
}, [table.id, updateTable]);
const isMustDisplayedField = useCallback(
(field: DBField) => {
return (
relationships.some(
(relationship) =>
relationship.sourceFieldId === field.id ||
relationship.targetFieldId === field.id
) || field.primaryKey
);
},
[relationships]
);
const relatedFieldIds = useMemo(() => {
const fieldIds = new Set<string>();
relationships.forEach((rel) => {
if (rel.sourceFieldId) fieldIds.add(rel.sourceFieldId);
if (rel.targetFieldId) fieldIds.add(rel.targetFieldId);
});
return fieldIds;
}, [relationships]);
const visibleFields = useMemo(() => {
if (expanded) {
if (expanded || fields.length <= TABLE_MINIMIZED_FIELDS) {
return fields;
}
const mustDisplayedFields = fields.filter((field: DBField) =>
isMustDisplayedField(field)
);
const nonMustDisplayedFields = fields.filter(
(field: DBField) => !isMustDisplayedField(field)
);
const mustDisplayedFields: DBField[] = [];
const nonMustDisplayedFields: DBField[] = [];
for (const field of fields) {
if (relatedFieldIds.has(field.id) || field.primaryKey) {
mustDisplayedFields.push(field);
} else {
nonMustDisplayedFields.push(field);
}
}
// Take required fields up to limit
const visibleMustDisplayedFields = mustDisplayedFields.slice(
0,
TABLE_MINIMIZED_FIELDS
);
const remainingSlots =
TABLE_MINIMIZED_FIELDS - visibleMustDisplayedFields.length;
const visibleNonMustDisplayedFields = nonMustDisplayedFields.slice(
0,
remainingSlots
);
return [
// Fill remaining slots with non-required fields
const visibleNonMustDisplayedFields =
remainingSlots > 0
? nonMustDisplayedFields.slice(0, remainingSlots)
: [];
// Combine and maintain original order
const visibleFieldsSet = new Set([
...visibleMustDisplayedFields,
...visibleNonMustDisplayedFields,
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
}, [expanded, fields, isMustDisplayedField]);
]);
const result = fields.filter((field) =>
visibleFieldsSet.has(field)
);
return result;
}, [expanded, fields, relatedFieldIds]);
const editTableName = useCallback(() => {
if (!editMode) return;
@@ -206,10 +276,10 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
useKeyPressEvent('Enter', editTableName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
const enterEditMode = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
}, []);
React.useEffect(() => {
if (table.name.trim()) {
@@ -217,35 +287,46 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
}
}, [table.name]);
const tableClassName = useMemo(
() =>
cn(
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700',
isOverlapping
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2'
: '',
!highlightOverlappingTables && isOverlapping
? 'animate-scale'
: '',
highlightOverlappingTables && isOverlapping
? 'animate-scale-2'
: '',
isDiffTableChanged && !isDiffNewTable && !isDiffTableRemoved
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
: '',
isDiffNewTable
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
: '',
isDiffTableRemoved
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
: ''
),
[
selected,
isOverlapping,
highlightOverlappingTables,
isDiffTableChanged,
isDiffNewTable,
isDiffTableRemoved,
]
);
return (
<TableNodeContextMenu table={table}>
<div
className={cn(
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700',
isOverlapping
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2'
: '',
!highlightOverlappingTables && isOverlapping
? 'animate-scale'
: '',
highlightOverlappingTables && isOverlapping
? 'animate-scale-2'
: '',
isDiffTableChanged &&
!isDiffNewTable &&
!isDiffTableRemoved
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
: '',
isDiffNewTable
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
: '',
isDiffTableRemoved
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
: ''
)}
className={tableClassName}
onClick={(e) => {
if (e.detail === 2) {
openTableInEditor();
@@ -421,20 +502,14 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
}}
>
{fields.map((field: DBField) => (
{visibleFields.map((field: DBField) => (
<TableNodeField
key={field.id}
focused={focused}
tableNodeId={id}
field={field}
highlighted={selectedRelEdges.some(
(edge) =>
edge.data?.relationship
.sourceFieldId === field.id ||
edge.data?.relationship
.targetFieldId === field.id
)}
visible={visibleFields.includes(field)}
highlighted={highlightedFieldIds.has(field.id)}
visible={true}
isConnectable={!table.isView}
/>
))}