@tldiagram/core-ui 1.91.0 → 1.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/api/client.d.ts +13 -1
  2. package/dist/components/ElementNode.d.ts +9 -0
  3. package/dist/config/runtime-vscode.d.ts +1 -0
  4. package/dist/config/runtime.d.ts +1 -0
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +10063 -9512
  7. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +2 -1
  8. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +20 -21
  9. package/dist/shims/empty-node-module.d.ts +2 -0
  10. package/dist/store/useStore.d.ts +78 -0
  11. package/dist/store/useStore.test.d.ts +1 -0
  12. package/package.json +7 -4
  13. package/src/App.tsx +0 -4
  14. package/src/api/client.ts +39 -1
  15. package/src/components/ElementNode.tsx +11 -58
  16. package/src/components/ElementPanel.tsx +2 -2
  17. package/src/components/LayoutSection.tsx +68 -93
  18. package/src/components/ViewGridNode.tsx +1 -4
  19. package/src/components/ZUI/renderer.ts +166 -66
  20. package/src/components/ZUI/useZUIInteraction.ts +235 -81
  21. package/src/config/runtime-vscode.ts +6 -0
  22. package/src/config/runtime.ts +4 -0
  23. package/src/index.ts +0 -1
  24. package/src/main.tsx +26 -14
  25. package/src/pages/ViewEditor/context.tsx +12 -4
  26. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +172 -121
  27. package/src/pages/ViewEditor/hooks/useViewData.ts +455 -253
  28. package/src/pages/ViewEditor/index.tsx +45 -32
  29. package/src/shims/empty-node-module.ts +1 -0
  30. package/src/store/useStore.test.ts +272 -0
  31. package/src/store/useStore.ts +285 -0
  32. package/dist/demo/DemoPage.d.ts +0 -9
  33. package/dist/demo/seed.d.ts +0 -9
  34. package/dist/demo/store.d.ts +0 -137
  35. package/src/demo/DemoPage.tsx +0 -184
  36. package/src/demo/seed.ts +0 -67
  37. package/src/demo/store.ts +0 -536
@@ -1,6 +1,7 @@
1
1
  import type { DrawingCanvasHandle } from '../../../components/DrawingCanvas';
2
2
  import { type Connection, type Edge as RFEdge, type EdgeChange, type Node as RFNode, type NodeChange, type NodeDragHandler, type OnConnect, type OnConnectStartParams } from 'reactflow';
3
3
  import type { Connector, PlacedElement, ViewTreeNode, LibraryElement, ViewLayer, ViewConnector, IncomingViewConnector } from '../../../types';
4
+ export declare function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nodes: RFNode[]): RFNode[];
4
5
  interface CanvasInteractionOptions {
5
6
  viewId: number | null;
6
7
  canEdit: boolean;
@@ -79,7 +80,7 @@ type HandleReconnectDragState = {
79
80
  hoveredNodeId?: string;
80
81
  hoveredHandleId?: string;
81
82
  };
82
- export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _drawingMode, isMobileLayout: _isMobileLayout, rfNodesRef, rfEdgesRef: _rfEdgesRef, viewElementsRef, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, containerRef, interactionSourceIdRef, hoveredZoomRef, hoverPanLockedUntilRef, setViewElements, setConnectors, setRfNodes, setRfEdges, setLinksMap, setParentLinksMap: _setParentLinksMap, setHoveredZoom, refreshGrid, refreshElements, stableOnConnectTo, existingElementIds, linksMapRef, parentLinksMapRef, openElementPanel: _openElementPanel, closeElementPanel: closeElementPanel, openConnectorPanel: openConnectorPanel, closeConnectorPanel: closeConnectorPanel, selectedElement, selectedEdgeId, connectors, layers, setSelectedElement, setSelectedEdge, setSelectedEdgeId, setSelectedProxyConnectorDetails, openProxyConnectorPanel, closeProxyConnectorPanel, handleElementDeleted, handleElementPermanentlyDeleted, handleConnectorDeleted, handleUpdateTags, drawingCanvasRef, snapToGrid, onMoveStateChange, }: CanvasInteractionOptions): {
83
+ export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _drawingMode, isMobileLayout: _isMobileLayout, rfNodesRef, rfEdgesRef: _rfEdgesRef, viewElementsRef, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, containerRef, interactionSourceIdRef, hoveredZoomRef, hoverPanLockedUntilRef, setViewElements: _setViewElements, setConnectors: _setConnectors, setRfNodes, setRfEdges, setLinksMap, setParentLinksMap: _setParentLinksMap, setHoveredZoom, refreshGrid, refreshElements, stableOnConnectTo, existingElementIds, linksMapRef, parentLinksMapRef, openElementPanel: _openElementPanel, closeElementPanel: closeElementPanel, openConnectorPanel: openConnectorPanel, closeConnectorPanel: closeConnectorPanel, selectedElement, selectedEdgeId, connectors, layers, setSelectedElement, setSelectedEdge, setSelectedEdgeId, setSelectedProxyConnectorDetails, openProxyConnectorPanel, closeProxyConnectorPanel, handleElementDeleted, handleElementPermanentlyDeleted, handleConnectorDeleted, handleUpdateTags, drawingCanvasRef, snapToGrid, onMoveStateChange, }: CanvasInteractionOptions): {
83
84
  canvasMenu: {
84
85
  x: number;
85
86
  y: number;
@@ -1,5 +1,4 @@
1
- import { type Edge as RFEdge, type Node as RFNode } from 'reactflow';
2
- import type { ViewTreeNode, PlacedElement, LibraryElement, Connector, IncomingViewConnector, ViewConnector, Tag } from '../../../types';
1
+ import type { ViewTreeNode, PlacedElement, LibraryElement, Connector, Tag } from '../../../types';
3
2
  interface ViewDataOptions {
4
3
  viewId: number | null;
5
4
  interactionSourceId: number | null;
@@ -37,38 +36,38 @@ interface ViewDataOptions {
37
36
  }
38
37
  export declare function useViewData({ viewId, interactionSourceId, clickConnectMode, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, hoveredLayerColor, tagColors, stableOnZoomIn, stableOnZoomOut, stableOnNavigateToView, stableOnSelect, stableOnOpenCodePreview, stableOnInteractionStart, stableOnConnectTo, stableOnStartHandleReconnect, stableOnRemoveElement, stableOnHoverZoom, hoveredZoomRef, }: ViewDataOptions): {
39
38
  view: ViewTreeNode | null | undefined;
40
- setView: import("react").Dispatch<import("react").SetStateAction<ViewTreeNode | null | undefined>>;
39
+ setView: (view: ViewTreeNode | null | undefined) => void;
41
40
  viewElements: PlacedElement[];
42
- setViewElements: import("react").Dispatch<import("react").SetStateAction<PlacedElement[]>>;
41
+ setViewElements: (next: import("../../../store/useStore").StoreSetter<PlacedElement[]>) => void;
43
42
  connectors: Connector[];
44
- setConnectors: import("react").Dispatch<import("react").SetStateAction<Connector[]>>;
45
- rfNodes: RFNode[];
46
- setRfNodes: import("react").Dispatch<import("react").SetStateAction<RFNode[]>>;
47
- rfEdges: RFEdge[];
48
- setRfEdges: import("react").Dispatch<import("react").SetStateAction<RFEdge[]>>;
49
- linksMap: Record<number, ViewConnector[]>;
50
- setLinksMap: import("react").Dispatch<import("react").SetStateAction<Record<number, ViewConnector[]>>>;
51
- parentLinksMap: Record<number, ViewConnector[]>;
52
- setParentLinksMap: import("react").Dispatch<import("react").SetStateAction<Record<number, ViewConnector[]>>>;
53
- incomingLinks: IncomingViewConnector[];
43
+ setConnectors: (next: import("../../../store/useStore").StoreSetter<Connector[]>) => void;
44
+ rfNodes: import("reactflow").Node[];
45
+ setRfNodes: (next: import("../../../store/useStore").StoreSetter<import("reactflow").Node[]>) => void;
46
+ rfEdges: import("reactflow").Edge[];
47
+ setRfEdges: (next: import("../../../store/useStore").StoreSetter<import("reactflow").Edge[]>) => void;
48
+ linksMap: Record<number, import("../../..").ViewConnector[]>;
49
+ setLinksMap: (next: import("../../../store/useStore").StoreSetter<Record<number, import("../../..").ViewConnector[]>>) => void;
50
+ parentLinksMap: Record<number, import("../../..").ViewConnector[]>;
51
+ setParentLinksMap: (next: import("../../../store/useStore").StoreSetter<Record<number, import("../../..").ViewConnector[]>>) => void;
52
+ incomingLinks: import("../../..").IncomingViewConnector[];
54
53
  treeData: ViewTreeNode[];
55
54
  allElements: LibraryElement[];
56
55
  libraryRefresh: number;
57
- setLibraryRefresh: import("react").Dispatch<import("react").SetStateAction<number>>;
56
+ setLibraryRefresh: (next: import("../../../store/useStore").StoreSetter<number>) => void;
58
57
  existingElementIds: Set<number>;
59
58
  viewElementsRef: import("react").MutableRefObject<PlacedElement[]>;
60
- linksMapRef: import("react").MutableRefObject<Record<number, ViewConnector[]>>;
61
- parentLinksMapRef: import("react").MutableRefObject<Record<number, ViewConnector[]>>;
62
- incomingLinksRef: import("react").MutableRefObject<IncomingViewConnector[]>;
59
+ linksMapRef: import("react").MutableRefObject<Record<number, import("../../..").ViewConnector[]>>;
60
+ parentLinksMapRef: import("react").MutableRefObject<Record<number, import("../../..").ViewConnector[]>>;
61
+ incomingLinksRef: import("react").MutableRefObject<import("../../..").IncomingViewConnector[]>;
63
62
  treeDataRef: import("react").MutableRefObject<ViewTreeNode[]>;
64
- rfNodesRef: import("react").MutableRefObject<RFNode[]>;
65
- rfEdgesRef: import("react").MutableRefObject<RFEdge[]>;
63
+ rfNodesRef: import("react").MutableRefObject<import("reactflow").Node[]>;
64
+ rfEdgesRef: import("react").MutableRefObject<import("reactflow").Edge[]>;
66
65
  viewIdRef: import("react").MutableRefObject<number | null>;
67
66
  refreshGrid: () => Promise<void>;
68
67
  refreshElements: () => Promise<void>;
69
68
  handleElementDeleted: (deletedId: number) => void;
70
69
  handleElementPermanentlyDeleted: (deletedId: number) => void;
71
70
  handleElementSaved: (saved: LibraryElement) => void;
72
- setAllElements: import("react").Dispatch<import("react").SetStateAction<LibraryElement[]>>;
71
+ setAllElements: (next: import("../../../store/useStore").StoreSetter<LibraryElement[]>) => void;
73
72
  };
74
73
  export {};
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,78 @@
1
+ import type { Edge as RFEdge, Node as RFNode } from 'reactflow';
2
+ import type { Connector, IncomingViewConnector, LibraryElement, PlacedElement, ViewConnector, ViewTreeNode } from '../types';
3
+ export type StoreSetter<T> = T | ((previous: T) => T);
4
+ export type ViewEditorUiState = {
5
+ viewId: number | null;
6
+ canEdit: boolean;
7
+ isOwner: boolean;
8
+ isFreePlan: boolean;
9
+ snapToGrid: boolean;
10
+ selectedElement: LibraryElement | null;
11
+ selectedConnector: Connector | null;
12
+ };
13
+ export type ViewContentLinks = {
14
+ linksMap: Record<number, ViewConnector[]>;
15
+ parentLinksMap: Record<number, ViewConnector[]>;
16
+ incomingLinks: IncomingViewConnector[];
17
+ };
18
+ export type ViewContentPayload = ViewContentLinks & {
19
+ view: ViewTreeNode | null;
20
+ viewElements: PlacedElement[];
21
+ connectors: Connector[];
22
+ treeData: ViewTreeNode[];
23
+ };
24
+ export type CanvasStoreState = ViewEditorUiState & {
25
+ view: ViewTreeNode | null | undefined;
26
+ viewElements: PlacedElement[];
27
+ connectors: Connector[];
28
+ nodes: RFNode[];
29
+ edges: RFEdge[];
30
+ linksMap: Record<number, ViewConnector[]>;
31
+ parentLinksMap: Record<number, ViewConnector[]>;
32
+ incomingLinks: IncomingViewConnector[];
33
+ treeData: ViewTreeNode[];
34
+ allElements: LibraryElement[];
35
+ libraryRefresh: number;
36
+ setViewEditorUi: (patch: Partial<ViewEditorUiState>) => void;
37
+ setSnapToGrid: (snapToGrid: boolean) => void;
38
+ setSelectedElement: (selectedElement: LibraryElement | null) => void;
39
+ setSelectedConnector: (selectedConnector: Connector | null) => void;
40
+ setView: (view: ViewTreeNode | null | undefined) => void;
41
+ setViewElements: (next: StoreSetter<PlacedElement[]>) => void;
42
+ setConnectors: (next: StoreSetter<Connector[]>) => void;
43
+ setNodes: (next: StoreSetter<RFNode[]>) => void;
44
+ setEdges: (next: StoreSetter<RFEdge[]>) => void;
45
+ setLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void;
46
+ setParentLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void;
47
+ setIncomingLinks: (next: StoreSetter<IncomingViewConnector[]>) => void;
48
+ setTreeData: (next: StoreSetter<ViewTreeNode[]>) => void;
49
+ setAllElements: (next: StoreSetter<LibraryElement[]>) => void;
50
+ setLibraryRefresh: (next: StoreSetter<number>) => void;
51
+ resetCanvas: () => void;
52
+ hydrateViewContent: (payload: ViewContentPayload) => void;
53
+ updateElementPosition: (elementId: number, x: number, y: number) => void;
54
+ removeElementPlacement: (elementId: number) => void;
55
+ removeElementEverywhere: (elementId: number) => void;
56
+ mergeSavedElement: (saved: LibraryElement) => void;
57
+ upsertConnector: (connector: Connector) => void;
58
+ replaceConnector: (connector: Connector) => void;
59
+ removeConnector: (connectorId: number) => void;
60
+ };
61
+ export declare const emptyViewEditorUiState: ViewEditorUiState;
62
+ export declare function findViewByOwner(nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null;
63
+ export declare function findViewPath(nodes: ViewTreeNode[], targetId: number, path?: ViewTreeNode[]): ViewTreeNode[] | null;
64
+ export declare function buildViewContentLinks(tree: ViewTreeNode[], viewId: number, viewElements: PlacedElement[]): ViewContentLinks;
65
+ export declare function selectExistingElementIds(state: Pick<CanvasStoreState, 'viewElements'>): Set<number>;
66
+ export declare function selectElementById(state: Pick<CanvasStoreState, 'viewElements'>, elementId: number): PlacedElement | undefined;
67
+ export declare function selectConnectorById(state: Pick<CanvasStoreState, 'connectors'>, connectorId: number): Connector | undefined;
68
+ export declare function updatePlacedElementPosition(elements: PlacedElement[], elementId: number, x: number, y: number): PlacedElement[];
69
+ export declare function removePlacedElement(elements: PlacedElement[], elementId: number): PlacedElement[];
70
+ export declare function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[];
71
+ export declare function upsertConnectorInList(connectors: Connector[], connector: Connector): Connector[];
72
+ export declare function removeConnectorFromList(connectors: Connector[], connectorId: number): Connector[];
73
+ export declare const useStore: import("zustand").UseBoundStore<import("zustand").StoreApi<CanvasStoreState>>;
74
+ export declare const canvasSelectors: {
75
+ existingElementIds: typeof selectExistingElementIds;
76
+ elementById: typeof selectElementById;
77
+ connectorById: typeof selectConnectorById;
78
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tldiagram/core-ui",
3
- "version": "1.91.0",
3
+ "version": "1.93.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,9 @@
38
38
  "dependencies": {
39
39
  "@buf/tldiagramcom_diagram.bufbuild_es": "^2.11.0-20260419172603-8192c519478e.1",
40
40
  "@bufbuild/protobuf": "^2.11.0",
41
- "esbuild": "^0.25.12"
41
+ "@tanstack/react-query": "^5.100.1",
42
+ "esbuild": "^0.25.12",
43
+ "zustand": "^5.0.12"
42
44
  },
43
45
  "peerDependencies": {
44
46
  "@chakra-ui/icons": "^2.2.4",
@@ -59,7 +61,7 @@
59
61
  "@emotion/styled": "^11.14.0",
60
62
  "@uiw/react-codemirror": "^4.25.8",
61
63
  "d3-force": "^3.0.0",
62
- "elkjs": "^0.11.1",
64
+ "dagre": "^0.8.5",
63
65
  "framer-motion": "^11.18.2",
64
66
  "html-to-image": "^1.11.13",
65
67
  "mermaid-ast": "^0.8.2",
@@ -125,12 +127,13 @@
125
127
  "@emotion/react": "^11.14.0",
126
128
  "@emotion/styled": "^11.14.0",
127
129
  "@eslint/js": "^9.0.0",
130
+ "@types/dagre": "^0.7.54",
128
131
  "@types/react": "^18.3.12",
129
132
  "@types/react-dom": "^18.3.1",
130
133
  "@uiw/react-codemirror": "^4.25.8",
131
134
  "@vitejs/plugin-react": "^4.3.4",
132
135
  "d3-force": "^3.0.0",
133
- "elkjs": "^0.11.1",
136
+ "dagre": "^0.8.5",
134
137
  "eslint": "^9.0.0",
135
138
  "eslint-plugin-react-hooks": "^5.0.0",
136
139
  "eslint-plugin-react-refresh": "^0.4.0",
package/src/App.tsx CHANGED
@@ -10,7 +10,6 @@ import Settings from './pages/Settings'
10
10
  import AppearanceSettings from './pages/AppearanceSettings'
11
11
  import { HeaderProvider, useHeader } from './components/HeaderContext'
12
12
  import TopMenuBar from './components/TopMenuBar'
13
- import DemoPage, { DemoNavigator } from './demo/DemoPage'
14
13
  import { ThemeProvider } from './context/ThemeContext'
15
14
  import { ACCENT_DEFAULT, BACKGROUND_DEFAULT, ELEMENT_DEFAULT, hexToRgba } from './constants/colors'
16
15
  import { platform } from './platform/local'
@@ -111,9 +110,6 @@ export default function App() {
111
110
  {platform.getRoutes({ user: null })}
112
111
 
113
112
  <Route path="/explore/shared/:token" element={<Box h="100vh" overflow="hidden"><HeaderProvider><SharedInfiniteZoom /></HeaderProvider></Box>} />
114
- <Route path="/demo" element={<DemoNavigator />} />
115
- <Route path="/demo/:id" element={<DemoPage />} />
116
-
117
113
  <Route
118
114
  element={
119
115
  <HeaderProvider>
package/src/api/client.ts CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  ImportService,
47
47
  } from '@buf/tldiagramcom_diagram.bufbuild_es/diag/v1/import_service_pb'
48
48
  import { transport } from './transport'
49
+ import { apiUrl, fetchApiAsset } from '../config/runtime'
49
50
 
50
51
  export interface DependenciesResponse {
51
52
  elements: DependencyElement[]
@@ -383,6 +384,36 @@ export const api = {
383
384
  return (json.views ?? []).map(mapDiagram)
384
385
  }),
385
386
 
387
+ // Lazy: root-level views only. Use for sidebar first render on huge workspaces.
388
+ treeRoots: (opts: { limit?: number; offset?: number; search?: string } = {}): Promise<{ views: ViewTreeNode[]; totalCount: number }> =>
389
+ rpc(async () => {
390
+ const res = await workspaceClient.getWorkspace({
391
+ includeContent: false,
392
+ level: 0,
393
+ limit: opts.limit ?? 0,
394
+ offset: opts.offset ?? 0,
395
+ search: opts.search ?? '',
396
+ })
397
+ const json = j<{ views: ProtoDiagram[]; total_count?: number }>(GetWorkspaceResponseSchema, res)
398
+ return {
399
+ views: (json.views ?? []).map(mapDiagram),
400
+ totalCount: Number(json.total_count ?? 0),
401
+ }
402
+ }),
403
+
404
+ // Lazy: direct children of a parent view. Used on tree node expand.
405
+ treeChildren: (parentId: number, opts: { limit?: number; offset?: number } = {}): Promise<ViewTreeNode[]> =>
406
+ rpc(async () => {
407
+ const res = await workspaceClient.getWorkspace({
408
+ includeContent: false,
409
+ parentId,
410
+ limit: opts.limit ?? 0,
411
+ offset: opts.offset ?? 0,
412
+ })
413
+ const json = j<{ views: ProtoDiagram[] }>(GetWorkspaceResponseSchema, res)
414
+ return (json.views ?? []).map(mapDiagram)
415
+ }),
416
+
386
417
  get: (id: number): Promise<ViewTreeNode> =>
387
418
  rpc(async () => {
388
419
  const res = await workspaceClient.getView({ viewId: id })
@@ -422,7 +453,14 @@ export const api = {
422
453
  delete: (_orgId: string, id: number): Promise<void> =>
423
454
  rpc(async () => { await workspaceClient.deleteView({ orgId: '', viewId: id }) }),
424
455
 
425
- thumbnail: async (_id: number): Promise<string | null> => null,
456
+ thumbnail: async (id: number): Promise<string | null> => {
457
+ const res = await fetchApiAsset(apiUrl(`/views/${id}/thumbnail.svg`), {
458
+ headers: { Accept: 'image/svg+xml' },
459
+ })
460
+ if (!res.ok) return null
461
+ const svg = await res.text()
462
+ return URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }))
463
+ },
426
464
 
427
465
  placements: {
428
466
  list: (diagramId: number): Promise<ElementPlacement[]> =>
@@ -12,9 +12,6 @@ import { ZoomInIcon, ZoomOutIcon, TrashIcon as TrashSvg, EditIcon as EditSvg } f
12
12
  import { vscodeBridge } from '../lib/vscodeBridge'
13
13
  import type { ExtensionToWebviewMessage } from '../types/vscode-messages'
14
14
  import {
15
- DEFAULT_SOURCE_HANDLE_SIDE,
16
- DEFAULT_TARGET_HANDLE_SIDE,
17
- ensureVisualHandleId,
18
15
  getVisualHandleId,
19
16
  getVisualHandleStyle,
20
17
  HANDLE_SLOT_CENTER_INDEX,
@@ -154,6 +151,10 @@ interface NodeData extends PlacedElement {
154
151
  layerHighlightColor?: string
155
152
  forceShowTagPopup?: boolean
156
153
  isCanvasMoving?: boolean
154
+ connectedHandleIds?: readonly string[]
155
+ selectedHandleIds?: readonly string[]
156
+ reconnectCandidates?: readonly { handleId: string; edgeId: string; endpoint: 'source' | 'target'; selected: boolean }[]
157
+ isConnectorHighlighted?: boolean
157
158
  }
158
159
 
159
160
  interface Props {
@@ -285,61 +286,13 @@ function ElementNode({ data, selected }: Props) {
285
286
  const zoom = useStore(zoomSelector)
286
287
  useAccentColor()
287
288
 
288
- const nodeId = String(data.element_id)
289
- const isConnectorHighlighted = useStore((s) =>
290
- s.edges.some(e => e.selected && (e.source === nodeId || e.target === nodeId))
291
- )
292
- const { connectedHandleKey, selectedHandleKey } = useStore((s) => {
293
- const connectedHandles = new Set<string>()
294
- const selectedHandles = new Set<string>()
295
- for (const connector of s.edges) {
296
- if (connector.source === nodeId) {
297
- const targetNode = s.nodeInternals.get(connector.target)
298
- if (targetNode && targetNode.type !== 'ContextBoundaryElement' && targetNode.type !== 'contextNeighborNode') {
299
- const handleId = ensureVisualHandleId(connector.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE)
300
- if (handleId) {
301
- connectedHandles.add(handleId)
302
- if (connector.selected) selectedHandles.add(handleId)
303
- }
304
- }
305
- }
306
- if (connector.target === nodeId) {
307
- const sourceNode = s.nodeInternals.get(connector.source)
308
- if (sourceNode && sourceNode.type !== 'ContextBoundaryElement' && sourceNode.type !== 'contextNeighborNode') {
309
- const handleId = ensureVisualHandleId(connector.targetHandle, DEFAULT_TARGET_HANDLE_SIDE)
310
- if (handleId) {
311
- connectedHandles.add(handleId)
312
- if (connector.selected) selectedHandles.add(handleId)
313
- }
314
- }
315
- }
316
- }
317
- return {
318
- connectedHandleKey: Array.from(connectedHandles).sort().join('|'),
319
- selectedHandleKey: Array.from(selectedHandles).sort().join('|'),
320
- }
321
- })
322
- const handleReconnectCandidates = useStore((s) => {
323
- const candidates: Array<{ handleId: string; edgeId: string; endpoint: 'source' | 'target'; selected: boolean }> = []
324
- for (const connector of s.edges) {
325
- if (connector.source === nodeId) {
326
- const handleId = ensureVisualHandleId(connector.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE)
327
- if (handleId) candidates.push({ handleId, edgeId: connector.id, endpoint: 'source', selected: !!connector.selected })
328
- }
329
- if (connector.target === nodeId) {
330
- const handleId = ensureVisualHandleId(connector.targetHandle, DEFAULT_TARGET_HANDLE_SIDE)
331
- if (handleId) candidates.push({ handleId, edgeId: connector.id, endpoint: 'target', selected: !!connector.selected })
332
- }
333
- }
334
- return candidates
335
- })
336
289
  const connectedHandleIds = useMemo(
337
- () => new Set(connectedHandleKey ? connectedHandleKey.split('|') : []),
338
- [connectedHandleKey],
290
+ () => new Set(data.connectedHandleIds ?? []),
291
+ [data.connectedHandleIds],
339
292
  )
340
293
  const selectedHandleIds = useMemo(
341
- () => new Set(selectedHandleKey ? selectedHandleKey.split('|') : []),
342
- [selectedHandleKey],
294
+ () => new Set(data.selectedHandleIds ?? []),
295
+ [data.selectedHandleIds],
343
296
  )
344
297
  const activeSides = useMemo(() => {
345
298
  const sides = new Set<string>()
@@ -351,7 +304,7 @@ function ElementNode({ data, selected }: Props) {
351
304
  }, [connectedHandleIds])
352
305
  const reconnectCandidateByHandle = useMemo(() => {
353
306
  const next = new Map<string, { edgeId: string; endpoint: 'source' | 'target' }>()
354
- const sortedCandidates = [...handleReconnectCandidates].sort((left, right) => {
307
+ const sortedCandidates = [...(data.reconnectCandidates ?? [])].sort((left, right) => {
355
308
  if (left.selected !== right.selected) return left.selected ? -1 : 1
356
309
  return left.edgeId.localeCompare(right.edgeId)
357
310
  })
@@ -361,7 +314,7 @@ function ElementNode({ data, selected }: Props) {
361
314
  }
362
315
  }
363
316
  return next
364
- }, [handleReconnectCandidates])
317
+ }, [data.reconnectCandidates])
365
318
 
366
319
  const derivedPrimaryIconPath = (() => {
367
320
  const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
@@ -509,7 +462,7 @@ function ElementNode({ data, selected }: Props) {
509
462
  isSelected={selected}
510
463
  isSource={isSource}
511
464
  isTarget={isTarget}
512
- isConnectorHighlighted={isConnectorHighlighted}
465
+ isConnectorHighlighted={!!data.isConnectorHighlighted}
513
466
  minW="180px"
514
467
  maxW="230px"
515
468
  cursor={bodyCursor}
@@ -480,7 +480,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
480
480
  try {
481
481
  if (viewId != null) {
482
482
  await api.workspace.views.placements.remove(viewId, element.id)
483
- } else if (orgId) {
483
+ } else if (orgId !== undefined) {
484
484
  await api.elements.delete(orgId, element.id)
485
485
  }
486
486
  onDelete?.(element.id)
@@ -491,7 +491,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
491
491
  const handlePermanentDelete = async () => {
492
492
  if (isReadOnly || !element) return
493
493
  try {
494
- if (!orgId) return
494
+ if (orgId === undefined) return
495
495
  await api.elements.delete(orgId, element.id)
496
496
  onPermanentDelete?.(element.id)
497
497
  confirmPermanentDelete.onClose()