@tldiagram/core-ui 1.92.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.
- package/dist/api/client.d.ts +13 -1
- package/dist/components/ElementNode.d.ts +9 -0
- package/dist/config/runtime-vscode.d.ts +1 -0
- package/dist/config/runtime.d.ts +1 -0
- package/dist/index.js +10344 -9294
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +2 -1
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +20 -21
- package/dist/shims/empty-node-module.d.ts +2 -0
- package/dist/store/useStore.d.ts +78 -0
- package/dist/store/useStore.test.d.ts +1 -0
- package/package.json +7 -4
- package/src/api/client.ts +39 -1
- package/src/components/ElementNode.tsx +11 -58
- package/src/components/ElementPanel.tsx +2 -2
- package/src/components/LayoutSection.tsx +68 -93
- package/src/components/ViewGridNode.tsx +1 -4
- package/src/components/ZUI/renderer.ts +166 -66
- package/src/components/ZUI/useZUIInteraction.ts +235 -81
- package/src/config/runtime-vscode.ts +6 -0
- package/src/config/runtime.ts +4 -0
- package/src/main.tsx +26 -14
- package/src/pages/ViewEditor/context.tsx +12 -4
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +172 -121
- package/src/pages/ViewEditor/hooks/useViewData.ts +455 -253
- package/src/pages/ViewEditor/index.tsx +45 -32
- package/src/shims/empty-node-module.ts +1 -0
- package/src/store/useStore.test.ts +272 -0
- package/src/store/useStore.ts +285 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { MarkerType } from 'reactflow'
|
|
3
5
|
import { api } from '../../../api/client'
|
|
4
6
|
import type {
|
|
5
7
|
ViewTreeNode,
|
|
6
8
|
PlacedElement,
|
|
7
9
|
LibraryElement,
|
|
8
10
|
Connector,
|
|
9
|
-
IncomingViewConnector,
|
|
10
|
-
ViewConnector,
|
|
11
11
|
Tag,
|
|
12
12
|
} from '../../../types'
|
|
13
13
|
import {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getVisualHandleIdForGroup,
|
|
18
18
|
getVisualHandleSlot,
|
|
19
19
|
} from '../../../utils/edgeDistribution'
|
|
20
|
+
import { buildViewContentLinks, useStore } from '../../../store/useStore'
|
|
20
21
|
|
|
21
22
|
interface ViewDataOptions {
|
|
22
23
|
viewId: number | null
|
|
@@ -47,6 +48,106 @@ function alphaColor(color: string, opacity: number): string {
|
|
|
47
48
|
return `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, transparent)`
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// Stable style refs so unchanged nodes keep identical style references across renders,
|
|
52
|
+
// letting structural-sharing fast-path bail out without rebuilding the node.
|
|
53
|
+
const HIDDEN_STYLE: CSSProperties = { opacity: 0.1, pointerEvents: 'none' }
|
|
54
|
+
const SOFT_FOCUS_STYLE: CSSProperties = { opacity: 0.2 }
|
|
55
|
+
const EMPTY_ARRAY: readonly never[] = Object.freeze([])
|
|
56
|
+
const EMPTY_NODE_CONNECTION_META = Object.freeze({
|
|
57
|
+
key: '',
|
|
58
|
+
connectedHandleIds: EMPTY_ARRAY as readonly string[],
|
|
59
|
+
selectedHandleIds: EMPTY_ARRAY as readonly string[],
|
|
60
|
+
reconnectCandidates: EMPTY_ARRAY as readonly NodeReconnectCandidate[],
|
|
61
|
+
isConnectorHighlighted: false,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
type NodeReconnectCandidate = {
|
|
65
|
+
handleId: string
|
|
66
|
+
edgeId: string
|
|
67
|
+
endpoint: 'source' | 'target'
|
|
68
|
+
selected: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type NodeConnectionMeta = {
|
|
72
|
+
key: string
|
|
73
|
+
connectedHandleIds: readonly string[]
|
|
74
|
+
selectedHandleIds: readonly string[]
|
|
75
|
+
reconnectCandidates: readonly NodeReconnectCandidate[]
|
|
76
|
+
isConnectorHighlighted: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ConnectorLayout = {
|
|
80
|
+
connector: Connector
|
|
81
|
+
sourceHandle: string
|
|
82
|
+
targetHandle: string
|
|
83
|
+
sourceGroupIndex: number
|
|
84
|
+
sourceGroupCount: number
|
|
85
|
+
targetGroupIndex: number
|
|
86
|
+
targetGroupCount: number
|
|
87
|
+
sourceHandleSide: string
|
|
88
|
+
targetHandleSide: string
|
|
89
|
+
sourceHandleSlot: number
|
|
90
|
+
targetHandleSlot: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildConnectorLayouts(connectors: Connector[], elementMap: Map<number, PlacedElement>): ConnectorLayout[] {
|
|
94
|
+
const filtered = connectors.filter((connector) =>
|
|
95
|
+
elementMap.has(connector.source_element_id) && elementMap.has(connector.target_element_id),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const handleUsage: Record<string, { id: string; type: 'source' | 'target'; otherNodeCoord: number }[]> = {}
|
|
99
|
+
filtered.forEach((connector) => {
|
|
100
|
+
const srcNode = elementMap.get(connector.source_element_id)
|
|
101
|
+
const tgtNode = elementMap.get(connector.target_element_id)
|
|
102
|
+
if (!srcNode || !tgtNode) return
|
|
103
|
+
|
|
104
|
+
const sourceSide = getLogicalHandleId(connector.source_handle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
|
|
105
|
+
const targetSide = getLogicalHandleId(connector.target_handle, DEFAULT_TARGET_HANDLE_SIDE) ?? DEFAULT_TARGET_HANDLE_SIDE
|
|
106
|
+
|
|
107
|
+
const srcKey = `${connector.source_element_id}-${sourceSide}`
|
|
108
|
+
handleUsage[srcKey] ??= []
|
|
109
|
+
const srcCoord = (sourceSide === 'left' || sourceSide === 'right') ? (tgtNode.position_y ?? 0) : (tgtNode.position_x ?? 0)
|
|
110
|
+
handleUsage[srcKey].push({ id: String(connector.id), type: 'source', otherNodeCoord: srcCoord })
|
|
111
|
+
|
|
112
|
+
const tgtKey = `${connector.target_element_id}-${targetSide}`
|
|
113
|
+
handleUsage[tgtKey] ??= []
|
|
114
|
+
const tgtCoord = (targetSide === 'left' || targetSide === 'right') ? (srcNode.position_y ?? 0) : (srcNode.position_x ?? 0)
|
|
115
|
+
handleUsage[tgtKey].push({ id: String(connector.id), type: 'target', otherNodeCoord: tgtCoord })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
Object.values(handleUsage).forEach((usages) => {
|
|
119
|
+
usages.sort((a, b) => a.otherNodeCoord - b.otherNodeCoord)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return filtered.map((connector) => {
|
|
123
|
+
const edgeId = String(connector.id)
|
|
124
|
+
const sourceSide = getLogicalHandleId(connector.source_handle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
|
|
125
|
+
const targetSide = getLogicalHandleId(connector.target_handle, DEFAULT_TARGET_HANDLE_SIDE) ?? DEFAULT_TARGET_HANDLE_SIDE
|
|
126
|
+
const srcGroup = handleUsage[`${connector.source_element_id}-${sourceSide}`] ?? []
|
|
127
|
+
const tgtGroup = handleUsage[`${connector.target_element_id}-${targetSide}`] ?? []
|
|
128
|
+
const sourceGroupIndex = srcGroup.findIndex((usage) => usage.id === edgeId && usage.type === 'source')
|
|
129
|
+
const targetGroupIndex = tgtGroup.findIndex((usage) => usage.id === edgeId && usage.type === 'target')
|
|
130
|
+
const sourceGroupCount = Math.max(srcGroup.length, 1)
|
|
131
|
+
const targetGroupCount = Math.max(tgtGroup.length, 1)
|
|
132
|
+
const sourceHandleSlot = getVisualHandleSlot(sourceGroupIndex, sourceGroupCount)
|
|
133
|
+
const targetHandleSlot = getVisualHandleSlot(targetGroupIndex, targetGroupCount)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
connector,
|
|
137
|
+
sourceHandle: getVisualHandleIdForGroup(sourceSide, sourceGroupIndex, sourceGroupCount),
|
|
138
|
+
targetHandle: getVisualHandleIdForGroup(targetSide, targetGroupIndex, targetGroupCount),
|
|
139
|
+
sourceGroupIndex,
|
|
140
|
+
sourceGroupCount: srcGroup.length,
|
|
141
|
+
targetGroupIndex,
|
|
142
|
+
targetGroupCount: tgtGroup.length,
|
|
143
|
+
sourceHandleSide: sourceSide,
|
|
144
|
+
targetHandleSide: targetSide,
|
|
145
|
+
sourceHandleSlot,
|
|
146
|
+
targetHandleSlot,
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
50
151
|
export function useViewData({
|
|
51
152
|
viewId,
|
|
52
153
|
interactionSourceId,
|
|
@@ -69,17 +170,32 @@ export function useViewData({
|
|
|
69
170
|
stableOnHoverZoom,
|
|
70
171
|
hoveredZoomRef,
|
|
71
172
|
}: ViewDataOptions) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
173
|
+
const queryClient = useQueryClient()
|
|
174
|
+
const view = useStore((state) => state.view)
|
|
175
|
+
const setView = useStore((state) => state.setView)
|
|
176
|
+
const viewElements = useStore((state) => state.viewElements)
|
|
177
|
+
const setViewElements = useStore((state) => state.setViewElements)
|
|
178
|
+
const connectors = useStore((state) => state.connectors)
|
|
179
|
+
const setConnectors = useStore((state) => state.setConnectors)
|
|
180
|
+
const rfNodes = useStore((state) => state.nodes)
|
|
181
|
+
const setRfNodes = useStore((state) => state.setNodes)
|
|
182
|
+
const rfEdges = useStore((state) => state.edges)
|
|
183
|
+
const setRfEdges = useStore((state) => state.setEdges)
|
|
184
|
+
const linksMap = useStore((state) => state.linksMap)
|
|
185
|
+
const setLinksMap = useStore((state) => state.setLinksMap)
|
|
186
|
+
const parentLinksMap = useStore((state) => state.parentLinksMap)
|
|
187
|
+
const setParentLinksMap = useStore((state) => state.setParentLinksMap)
|
|
188
|
+
const incomingLinks = useStore((state) => state.incomingLinks)
|
|
189
|
+
const treeData = useStore((state) => state.treeData)
|
|
190
|
+
const allElements = useStore((state) => state.allElements)
|
|
191
|
+
const setAllElements = useStore((state) => state.setAllElements)
|
|
192
|
+
const libraryRefresh = useStore((state) => state.libraryRefresh)
|
|
193
|
+
const setLibraryRefresh = useStore((state) => state.setLibraryRefresh)
|
|
194
|
+
const hydrateViewContent = useStore((state) => state.hydrateViewContent)
|
|
195
|
+
const resetCanvas = useStore((state) => state.resetCanvas)
|
|
196
|
+
const removeElementPlacement = useStore((state) => state.removeElementPlacement)
|
|
197
|
+
const removeElementEverywhere = useStore((state) => state.removeElementEverywhere)
|
|
198
|
+
const mergeSavedElement = useStore((state) => state.mergeSavedElement)
|
|
83
199
|
|
|
84
200
|
// Mutable refs for stable callbacks
|
|
85
201
|
const viewElementsRef = useRef(viewElements)
|
|
@@ -101,158 +217,90 @@ export function useViewData({
|
|
|
101
217
|
|
|
102
218
|
// ── Fetch tree ─────────────────────────────────────────────────────────────
|
|
103
219
|
const refreshGrid = useCallback(async () => {
|
|
104
|
-
const tree = await
|
|
105
|
-
|
|
106
|
-
|
|
220
|
+
const tree = await queryClient.fetchQuery({
|
|
221
|
+
queryKey: ['workspace', 'views', 'tree'],
|
|
222
|
+
queryFn: () => api.workspace.views.tree(),
|
|
223
|
+
staleTime: 0,
|
|
224
|
+
}).catch(() => null)
|
|
225
|
+
if (tree) useStore.getState().setTreeData(tree)
|
|
226
|
+
}, [queryClient])
|
|
107
227
|
|
|
108
228
|
// ── Fetch view content ──────────────────────────────────────────────────
|
|
229
|
+
const viewContentQuery = useQuery({
|
|
230
|
+
queryKey: ['workspace', 'views', viewId, 'editor-content'],
|
|
231
|
+
enabled: viewId !== null,
|
|
232
|
+
queryFn: async () => {
|
|
233
|
+
if (viewId === null) throw new Error('Missing view id')
|
|
234
|
+
const [diag, content, tree] = await Promise.all([
|
|
235
|
+
api.workspace.views.get(viewId),
|
|
236
|
+
api.workspace.views.content(viewId),
|
|
237
|
+
api.workspace.views.tree(),
|
|
238
|
+
])
|
|
239
|
+
const viewElements = content.placements || []
|
|
240
|
+
const connectors = content.connectors || []
|
|
241
|
+
return {
|
|
242
|
+
view: diag,
|
|
243
|
+
viewElements,
|
|
244
|
+
connectors,
|
|
245
|
+
treeData: tree,
|
|
246
|
+
...buildViewContentLinks(tree, viewId, viewElements),
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
109
251
|
useEffect(() => {
|
|
110
252
|
if (viewId === null) return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const load = async () => {
|
|
114
|
-
try {
|
|
115
|
-
const [diag, content, tree] = await Promise.all([
|
|
116
|
-
api.workspace.views.get(viewId),
|
|
117
|
-
api.workspace.views.content(viewId),
|
|
118
|
-
api.workspace.views.tree(),
|
|
119
|
-
])
|
|
120
|
-
if (!active) return
|
|
121
|
-
|
|
122
|
-
const safeObjs = content.placements || []
|
|
123
|
-
const safeConnectors = content.connectors || []
|
|
124
|
-
|
|
125
|
-
const linksObj: Record<number, ViewConnector[]> = {}
|
|
126
|
-
const parentLinksObj: Record<number, ViewConnector[]> = {}
|
|
127
|
-
|
|
128
|
-
// Helper: recursively find nodes in tree that are owned by elements on canvas (zoom-in)
|
|
129
|
-
// OR the parent view of the current view (zoom-out)
|
|
130
|
-
const findViewByOwner = (nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null => {
|
|
131
|
-
for (const node of nodes) {
|
|
132
|
-
if (node.owner_element_id !== null && Number(node.owner_element_id) === Number(elementId)) return node
|
|
133
|
-
const found = findViewByOwner(node.children, elementId)
|
|
134
|
-
if (found) return found
|
|
135
|
-
}
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const findViewPath = (nodes: ViewTreeNode[], targetId: number, path: ViewTreeNode[] = []): ViewTreeNode[] | null => {
|
|
140
|
-
for (const node of nodes) {
|
|
141
|
-
if (node.id === targetId) return [...path, node]
|
|
142
|
-
const found = findViewPath(node.children, targetId, [...path, node])
|
|
143
|
-
if (found) return found
|
|
144
|
-
}
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
253
|
+
if (viewContentQuery.data) hydrateViewContent(viewContentQuery.data)
|
|
254
|
+
}, [hydrateViewContent, viewContentQuery.data, viewId])
|
|
147
255
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const incoming: IncomingViewConnector[] = []
|
|
153
|
-
if (parentView && currentViewInTree?.owner_element_id) {
|
|
154
|
-
incoming.push({
|
|
155
|
-
id: 0,
|
|
156
|
-
element_id: currentViewInTree.owner_element_id,
|
|
157
|
-
element_name: 'Parent', // Optional: could find name in parentView.placements
|
|
158
|
-
from_view_id: parentView.id,
|
|
159
|
-
from_view_name: parentView.name,
|
|
160
|
-
to_view_id: viewId,
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
for (const obj of safeObjs) {
|
|
165
|
-
// Child Link: if there exists a view owned by this element
|
|
166
|
-
const childView = findViewByOwner(tree, obj.element_id)
|
|
167
|
-
if (childView) {
|
|
168
|
-
linksObj[obj.element_id] = [{
|
|
169
|
-
id: 0,
|
|
170
|
-
element_id: obj.element_id,
|
|
171
|
-
from_view_id: viewId,
|
|
172
|
-
to_view_id: childView.id,
|
|
173
|
-
to_view_name: childView.name,
|
|
174
|
-
relation_type: 'child',
|
|
175
|
-
}]
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Parent Link: all elements in a view can 'zoom out' to its structural parent
|
|
179
|
-
if (parentView) {
|
|
180
|
-
parentLinksObj[obj.element_id] = [{
|
|
181
|
-
id: 0,
|
|
182
|
-
element_id: obj.element_id,
|
|
183
|
-
from_view_id: parentView.id, // we go TO the parentView, coming FROM the parentView context?
|
|
184
|
-
to_view_id: parentView.id,
|
|
185
|
-
to_view_name: parentView.name,
|
|
186
|
-
relation_type: 'parent',
|
|
187
|
-
}]
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
setLinksMap(linksObj)
|
|
192
|
-
setParentLinksMap(parentLinksObj)
|
|
193
|
-
setConnectors(safeConnectors)
|
|
194
|
-
setViewElements(safeObjs)
|
|
195
|
-
setIncomingLinks(incoming)
|
|
196
|
-
setView(diag)
|
|
197
|
-
setTreeData(tree)
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.error('DIAGRAM EDITOR LOAD ERROR:', err)
|
|
200
|
-
if (active) setView(null)
|
|
201
|
-
}
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (viewContentQuery.isError) {
|
|
258
|
+
console.error('DIAGRAM EDITOR LOAD ERROR:', viewContentQuery.error)
|
|
259
|
+
setView(null)
|
|
202
260
|
}
|
|
203
|
-
|
|
204
|
-
load()
|
|
205
|
-
return () => { active = false }
|
|
206
|
-
}, [viewId])
|
|
261
|
+
}, [setView, viewContentQuery.error, viewContentQuery.isError])
|
|
207
262
|
|
|
208
263
|
// ── Clear canvas on navigation ─────────────────────────────────────────────
|
|
209
264
|
useEffect(() => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}, [viewId])
|
|
265
|
+
resetCanvas()
|
|
266
|
+
}, [resetCanvas, viewId])
|
|
213
267
|
|
|
214
268
|
// ── Keep all-org elements for inline adder ──────────────────────────────────
|
|
269
|
+
const allElementsQuery = useQuery({
|
|
270
|
+
queryKey: ['elements', 'list', libraryRefresh],
|
|
271
|
+
queryFn: () => api.elements.list(),
|
|
272
|
+
})
|
|
273
|
+
|
|
215
274
|
useEffect(() => {
|
|
216
|
-
|
|
217
|
-
}, [
|
|
275
|
+
if (allElementsQuery.data) setAllElements(allElementsQuery.data)
|
|
276
|
+
}, [allElementsQuery.data, setAllElements])
|
|
218
277
|
|
|
219
278
|
// ── Refresh elements ────────────────────────────────────────────────────────
|
|
220
279
|
const refreshElements = useCallback(async () => {
|
|
221
280
|
if (viewId === null) return
|
|
222
|
-
const fresh = await
|
|
223
|
-
|
|
224
|
-
|
|
281
|
+
const fresh = await queryClient.fetchQuery({
|
|
282
|
+
queryKey: ['workspace', 'views', viewId, 'content'],
|
|
283
|
+
queryFn: () => api.workspace.views.content(viewId),
|
|
284
|
+
staleTime: 0,
|
|
285
|
+
}).catch(() => null)
|
|
286
|
+
if (fresh) {
|
|
287
|
+
setViewElements(fresh.placements)
|
|
288
|
+
setConnectors(fresh.connectors)
|
|
289
|
+
}
|
|
290
|
+
}, [queryClient, setConnectors, setViewElements, viewId])
|
|
225
291
|
|
|
226
|
-
// ──
|
|
292
|
+
// ── Element mutation helpers ───────────────────────────────────────────────
|
|
227
293
|
const handleElementDeleted = useCallback((deletedId: number) => {
|
|
228
|
-
|
|
229
|
-
}, [])
|
|
294
|
+
removeElementPlacement(deletedId)
|
|
295
|
+
}, [removeElementPlacement])
|
|
230
296
|
|
|
231
297
|
const handleElementPermanentlyDeleted = useCallback((deletedId: number) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}, [])
|
|
298
|
+
removeElementEverywhere(deletedId)
|
|
299
|
+
}, [removeElementEverywhere])
|
|
235
300
|
|
|
236
301
|
const handleElementSaved = useCallback((saved: LibraryElement) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
prev.map((o) =>
|
|
240
|
-
o.element_id === saved.id
|
|
241
|
-
? {
|
|
242
|
-
...o,
|
|
243
|
-
name: saved.name,
|
|
244
|
-
description: saved.description,
|
|
245
|
-
kind: saved.kind,
|
|
246
|
-
technology: saved.technology,
|
|
247
|
-
url: saved.url,
|
|
248
|
-
logo_url: saved.logo_url,
|
|
249
|
-
technology_connectors: saved.technology_connectors,
|
|
250
|
-
tags: saved.tags,
|
|
251
|
-
}
|
|
252
|
-
: o,
|
|
253
|
-
),
|
|
254
|
-
)
|
|
255
|
-
}, [])
|
|
302
|
+
mergeSavedElement(saved)
|
|
303
|
+
}, [mergeSavedElement])
|
|
256
304
|
|
|
257
305
|
// ── Stable element ID set ───────────────────────────────────────────────────
|
|
258
306
|
const existingElementIdsRef = useRef<Set<number>>(new Set())
|
|
@@ -268,51 +316,197 @@ export function useViewData({
|
|
|
268
316
|
return nextIds
|
|
269
317
|
}, [viewElements])
|
|
270
318
|
|
|
319
|
+
// Stable-ref fallback parent links: flatten only when the underlying map changes so
|
|
320
|
+
// nodes without their own parent link entry can still pass the data-equality fast path.
|
|
321
|
+
const viewParentLinks = useMemo(
|
|
322
|
+
() => Object.values(parentLinksMap).flat(),
|
|
323
|
+
[parentLinksMap],
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const parentViewId = useMemo(() => {
|
|
327
|
+
const findInTreeById = (nodes: ViewTreeNode[], id: number): ViewTreeNode | null => {
|
|
328
|
+
for (const node of nodes) {
|
|
329
|
+
if (node.id === id) return node
|
|
330
|
+
const found = findInTreeById(node.children, id)
|
|
331
|
+
if (found) return found
|
|
332
|
+
}
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
const currentView = findInTreeById(treeData, viewId || -1)
|
|
336
|
+
return currentView?.parent_view_id
|
|
337
|
+
}, [treeData, viewId])
|
|
338
|
+
|
|
339
|
+
const elementMap = useMemo(() => {
|
|
340
|
+
const next = new Map<number, PlacedElement>()
|
|
341
|
+
for (const element of viewElements) next.set(element.element_id, element)
|
|
342
|
+
return next
|
|
343
|
+
}, [viewElements])
|
|
344
|
+
|
|
345
|
+
const connectorLayouts = useMemo(
|
|
346
|
+
() => buildConnectorLayouts(connectors, elementMap),
|
|
347
|
+
[connectors, elementMap],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
const connectionMetaCacheRef = useRef<Map<number, NodeConnectionMeta>>(new Map())
|
|
351
|
+
const nodeConnectionMetaByElementId = useMemo(() => {
|
|
352
|
+
const drafts = new Map<number, {
|
|
353
|
+
connected: Set<string>
|
|
354
|
+
selected: Set<string>
|
|
355
|
+
reconnect: NodeReconnectCandidate[]
|
|
356
|
+
highlighted: boolean
|
|
357
|
+
}>()
|
|
358
|
+
|
|
359
|
+
const draftFor = (elementId: number) => {
|
|
360
|
+
let draft = drafts.get(elementId)
|
|
361
|
+
if (!draft) {
|
|
362
|
+
draft = { connected: new Set(), selected: new Set(), reconnect: [], highlighted: false }
|
|
363
|
+
drafts.set(elementId, draft)
|
|
364
|
+
}
|
|
365
|
+
return draft
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const selectedId = selectedEdgeId === null ? null : String(selectedEdgeId)
|
|
369
|
+
for (const layout of connectorLayouts) {
|
|
370
|
+
const connector = layout.connector
|
|
371
|
+
const edgeId = String(connector.id)
|
|
372
|
+
const isSelected = selectedId === edgeId
|
|
373
|
+
const sourceDraft = draftFor(connector.source_element_id)
|
|
374
|
+
const targetDraft = draftFor(connector.target_element_id)
|
|
375
|
+
|
|
376
|
+
sourceDraft.connected.add(layout.sourceHandle)
|
|
377
|
+
targetDraft.connected.add(layout.targetHandle)
|
|
378
|
+
sourceDraft.reconnect.push({ handleId: layout.sourceHandle, edgeId, endpoint: 'source', selected: isSelected })
|
|
379
|
+
targetDraft.reconnect.push({ handleId: layout.targetHandle, edgeId, endpoint: 'target', selected: isSelected })
|
|
380
|
+
|
|
381
|
+
if (isSelected) {
|
|
382
|
+
sourceDraft.selected.add(layout.sourceHandle)
|
|
383
|
+
targetDraft.selected.add(layout.targetHandle)
|
|
384
|
+
sourceDraft.highlighted = true
|
|
385
|
+
targetDraft.highlighted = true
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const prev = connectionMetaCacheRef.current
|
|
390
|
+
const next = new Map<number, NodeConnectionMeta>()
|
|
391
|
+
for (const [elementId, draft] of drafts) {
|
|
392
|
+
const connectedHandleIds = Array.from(draft.connected).sort()
|
|
393
|
+
const selectedHandleIds = Array.from(draft.selected).sort()
|
|
394
|
+
const reconnectCandidates = draft.reconnect.sort((left, right) => {
|
|
395
|
+
if (left.handleId !== right.handleId) return left.handleId.localeCompare(right.handleId)
|
|
396
|
+
if (left.selected !== right.selected) return left.selected ? -1 : 1
|
|
397
|
+
return left.edgeId.localeCompare(right.edgeId)
|
|
398
|
+
})
|
|
399
|
+
const reconnectKey = reconnectCandidates
|
|
400
|
+
.map((candidate) => `${candidate.handleId}:${candidate.edgeId}:${candidate.endpoint}:${candidate.selected ? 1 : 0}`)
|
|
401
|
+
.join(',')
|
|
402
|
+
const key = [
|
|
403
|
+
connectedHandleIds.join('|'),
|
|
404
|
+
selectedHandleIds.join('|'),
|
|
405
|
+
reconnectKey,
|
|
406
|
+
draft.highlighted ? 1 : 0,
|
|
407
|
+
].join('::')
|
|
408
|
+
const existing = prev.get(elementId)
|
|
409
|
+
next.set(elementId, existing?.key === key
|
|
410
|
+
? existing
|
|
411
|
+
: {
|
|
412
|
+
key,
|
|
413
|
+
connectedHandleIds,
|
|
414
|
+
selectedHandleIds,
|
|
415
|
+
reconnectCandidates,
|
|
416
|
+
isConnectorHighlighted: draft.highlighted,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
connectionMetaCacheRef.current = next
|
|
420
|
+
return next
|
|
421
|
+
}, [connectorLayouts, selectedEdgeId])
|
|
422
|
+
|
|
271
423
|
// ── Derive RF nodes ────────────────────────────────────────────────────────
|
|
272
424
|
useEffect(() => {
|
|
273
425
|
setRfNodes((prevNodes) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
if (node.id === id) return node
|
|
282
|
-
const found = findInTreeById(node.children, id)
|
|
283
|
-
if (found) return found
|
|
284
|
-
}
|
|
285
|
-
return null
|
|
286
|
-
}
|
|
287
|
-
const currentView = findInTreeById(treeData, viewId || -1)
|
|
288
|
-
const parentViewId = currentView?.parent_view_id
|
|
289
|
-
|
|
426
|
+
|
|
427
|
+
const prevNodeMap = new Map(prevNodes.map((n) => [n.id, n]))
|
|
428
|
+
const hiddenSet = hiddenLayerTags.length > 0 ? new Set(hiddenLayerTags) : null
|
|
429
|
+
const activeSet = activeTags.length > 0 ? new Set(activeTags) : null
|
|
430
|
+
const hoveredSet = hoveredLayerTags !== null ? new Set(hoveredLayerTags) : null
|
|
431
|
+
const isClickConnectMode = clickConnectMode !== null
|
|
432
|
+
|
|
290
433
|
return viewElements.map((obj) => {
|
|
291
|
-
const
|
|
434
|
+
const nodeId = String(obj.element_id)
|
|
435
|
+
const existing = prevNodeMap.get(nodeId)
|
|
292
436
|
const objTags = obj.tags || []
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
437
|
+
|
|
438
|
+
const isHiddenByLayer = hiddenSet !== null && objTags.some((t) => hiddenSet.has(t))
|
|
439
|
+
const isInactive = isHiddenByLayer || (activeSet !== null && !objTags.some((t) => activeSet.has(t)))
|
|
440
|
+
const isLayerHighlighted = hoveredSet !== null && objTags.some((t) => hoveredSet.has(t))
|
|
441
|
+
const isSoftFocused = hoveredSet !== null && !isLayerHighlighted
|
|
442
|
+
|
|
443
|
+
const newZIndex = isLayerHighlighted ? 10 : interactionSourceId === obj.element_id ? 1000 : 0
|
|
444
|
+
const newStyle = isInactive
|
|
445
|
+
? HIDDEN_STYLE
|
|
446
|
+
: isSoftFocused
|
|
447
|
+
? SOFT_FOCUS_STYLE
|
|
448
|
+
: undefined
|
|
449
|
+
const layerHighlightColor = isLayerHighlighted ? (hoveredLayerColor ?? undefined) : undefined
|
|
450
|
+
const position = existing?.dragging ? existing.position : { x: obj.position_x ?? 0, y: obj.position_y ?? 0 }
|
|
451
|
+
const isZoomHovered = hoveredZoomRef.current?.elementId === obj.element_id ? hoveredZoomRef.current.type : null
|
|
452
|
+
const links = linksMap[obj.element_id] || EMPTY_ARRAY
|
|
453
|
+
const parentLinks = parentLinksMap[obj.element_id] || viewParentLinks
|
|
454
|
+
const connectionMeta = nodeConnectionMetaByElementId.get(obj.element_id) ?? EMPTY_NODE_CONNECTION_META
|
|
455
|
+
|
|
456
|
+
// Structural sharing: if every input that would produce the same output matches the
|
|
457
|
+
// previous node, return the previous reference so React Flow skips this node's work.
|
|
458
|
+
if (
|
|
459
|
+
existing &&
|
|
460
|
+
existing.style === newStyle &&
|
|
461
|
+
existing.zIndex === newZIndex &&
|
|
462
|
+
existing.position.x === position.x &&
|
|
463
|
+
existing.position.y === position.y &&
|
|
464
|
+
existing.data &&
|
|
465
|
+
existing.data.element_id === obj.element_id &&
|
|
466
|
+
existing.data.tags === obj.tags &&
|
|
467
|
+
existing.data.name === obj.name &&
|
|
468
|
+
existing.data.position_x === obj.position_x &&
|
|
469
|
+
existing.data.position_y === obj.position_y &&
|
|
470
|
+
existing.data.description === obj.description &&
|
|
471
|
+
existing.data.kind === obj.kind &&
|
|
472
|
+
existing.data.technology === obj.technology &&
|
|
473
|
+
existing.data.url === obj.url &&
|
|
474
|
+
existing.data.logo_url === obj.logo_url &&
|
|
475
|
+
existing.data.repo === obj.repo &&
|
|
476
|
+
existing.data.branch === obj.branch &&
|
|
477
|
+
existing.data.file_path === obj.file_path &&
|
|
478
|
+
existing.data.technology_connectors === obj.technology_connectors &&
|
|
479
|
+
existing.data.links === links &&
|
|
480
|
+
existing.data.parentLinks === parentLinks &&
|
|
481
|
+
existing.data.parentViewId === parentViewId &&
|
|
482
|
+
existing.data.interactionSourceId === interactionSourceId &&
|
|
483
|
+
existing.data.isClickConnectMode === isClickConnectMode &&
|
|
484
|
+
existing.data.tagColors === tagColors &&
|
|
485
|
+
existing.data.layerHighlightColor === layerHighlightColor &&
|
|
486
|
+
existing.data.forceShowTagPopup === isLayerHighlighted &&
|
|
487
|
+
existing.data.isZoomHovered === isZoomHovered &&
|
|
488
|
+
existing.data.connectedHandleIds === connectionMeta.connectedHandleIds &&
|
|
489
|
+
existing.data.selectedHandleIds === connectionMeta.selectedHandleIds &&
|
|
490
|
+
existing.data.reconnectCandidates === connectionMeta.reconnectCandidates &&
|
|
491
|
+
existing.data.isConnectorHighlighted === connectionMeta.isConnectorHighlighted
|
|
492
|
+
) {
|
|
493
|
+
return existing
|
|
494
|
+
}
|
|
297
495
|
|
|
298
496
|
return {
|
|
299
|
-
id:
|
|
497
|
+
id: nodeId,
|
|
300
498
|
type: 'elementNode',
|
|
301
|
-
position
|
|
499
|
+
position,
|
|
302
500
|
width: existing?.width,
|
|
303
501
|
height: existing?.height,
|
|
304
502
|
selected: existing?.selected,
|
|
305
503
|
dragging: existing?.dragging,
|
|
306
|
-
zIndex:
|
|
307
|
-
style:
|
|
308
|
-
? { opacity: 0.1, pointerEvents: 'none' }
|
|
309
|
-
: isSoftFocused
|
|
310
|
-
? { opacity: 0.2 }
|
|
311
|
-
: undefined,
|
|
504
|
+
zIndex: newZIndex,
|
|
505
|
+
style: newStyle,
|
|
312
506
|
data: {
|
|
313
507
|
...obj,
|
|
314
|
-
links
|
|
315
|
-
parentLinks
|
|
508
|
+
links,
|
|
509
|
+
parentLinks,
|
|
316
510
|
parentViewId,
|
|
317
511
|
onZoomIn: stableOnZoomIn,
|
|
318
512
|
onZoomOut: stableOnZoomOut,
|
|
@@ -324,130 +518,138 @@ export function useViewData({
|
|
|
324
518
|
onStartHandleReconnect: stableOnStartHandleReconnect,
|
|
325
519
|
onRemove: stableOnRemoveElement,
|
|
326
520
|
onHoverZoom: stableOnHoverZoom,
|
|
327
|
-
isZoomHovered
|
|
521
|
+
isZoomHovered,
|
|
328
522
|
interactionSourceId,
|
|
329
|
-
isClickConnectMode
|
|
523
|
+
isClickConnectMode,
|
|
330
524
|
tagColors,
|
|
331
|
-
layerHighlightColor
|
|
525
|
+
layerHighlightColor,
|
|
332
526
|
forceShowTagPopup: isLayerHighlighted,
|
|
527
|
+
connectedHandleIds: connectionMeta.connectedHandleIds,
|
|
528
|
+
selectedHandleIds: connectionMeta.selectedHandleIds,
|
|
529
|
+
reconnectCandidates: connectionMeta.reconnectCandidates,
|
|
530
|
+
isConnectorHighlighted: connectionMeta.isConnectorHighlighted,
|
|
333
531
|
},
|
|
334
532
|
}
|
|
335
533
|
})
|
|
336
534
|
})
|
|
337
535
|
}, [
|
|
338
|
-
viewElements,
|
|
536
|
+
viewElements, linksMap, parentLinksMap, viewParentLinks, parentViewId,
|
|
339
537
|
interactionSourceId, clickConnectMode,
|
|
340
538
|
stableOnZoomIn, stableOnZoomOut, stableOnNavigateToView, stableOnSelect,
|
|
341
539
|
stableOnInteractionStart, stableOnConnectTo, stableOnStartHandleReconnect, stableOnRemoveElement, stableOnHoverZoom,
|
|
342
540
|
stableOnOpenCodePreview, hoveredZoomRef, activeTags, hiddenLayerTags, hoveredLayerTags, hoveredLayerColor, tagColors,
|
|
541
|
+
nodeConnectionMetaByElementId, setRfNodes,
|
|
343
542
|
])
|
|
344
543
|
|
|
345
544
|
// ── Derive RF connectors ────────────────────────────────────────────────────────
|
|
346
545
|
useEffect(() => {
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const handleUsage: Record<string, { id: string; type: 'source' | 'target'; otherNodeCoord: number }[]> = {}
|
|
351
|
-
filtered.forEach((c) => {
|
|
352
|
-
const srcNode = viewElements.find((o) => o.element_id === c.source_element_id)
|
|
353
|
-
const tgtNode = viewElements.find((o) => o.element_id === c.target_element_id)
|
|
354
|
-
if (!srcNode || !tgtNode) return
|
|
355
|
-
|
|
356
|
-
const sourceSide = getLogicalHandleId(c.source_handle, DEFAULT_SOURCE_HANDLE_SIDE)
|
|
357
|
-
const targetSide = getLogicalHandleId(c.target_handle, DEFAULT_TARGET_HANDLE_SIDE)
|
|
358
|
-
|
|
359
|
-
const srcKey = `${c.source_element_id}-${sourceSide}`
|
|
360
|
-
handleUsage[srcKey] ??= []
|
|
361
|
-
const srcCoord = (sourceSide === 'left' || sourceSide === 'right') ? (tgtNode.position_y ?? 0) : (tgtNode.position_x ?? 0)
|
|
362
|
-
handleUsage[srcKey].push({ id: String(c.id), type: 'source', otherNodeCoord: srcCoord })
|
|
363
|
-
|
|
364
|
-
const tgtKey = `${c.target_element_id}-${targetSide}`
|
|
365
|
-
handleUsage[tgtKey] ??= []
|
|
366
|
-
const tgtCoord = (targetSide === 'left' || targetSide === 'right') ? (srcNode.position_y ?? 0) : (srcNode.position_x ?? 0)
|
|
367
|
-
handleUsage[tgtKey].push({ id: String(c.id), type: 'target', otherNodeCoord: tgtCoord })
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
Object.values(handleUsage).forEach((usages) => {
|
|
371
|
-
usages.sort((a, b) => a.otherNodeCoord - b.otherNodeCoord)
|
|
372
|
-
})
|
|
546
|
+
const hiddenSet = hiddenLayerTags.length > 0 ? new Set(hiddenLayerTags) : null
|
|
547
|
+
const activeSet = activeTags.length > 0 ? new Set(activeTags) : null
|
|
548
|
+
const hoveredSet = hoveredLayerTags !== null ? new Set(hoveredLayerTags) : null
|
|
373
549
|
|
|
550
|
+
setRfEdges((prevConnectors) => {
|
|
551
|
+
const prevEdgeMap = new Map(prevConnectors.map((e) => [e.id, e]))
|
|
374
552
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
553
|
+
return connectorLayouts.map((layout) => {
|
|
554
|
+
const e = layout.connector
|
|
555
|
+
const edgeId = String(e.id)
|
|
556
|
+
const existing = prevEdgeMap.get(edgeId)
|
|
378
557
|
const dir = e.direction ?? 'forward'
|
|
379
|
-
const arrowMarker = { type: MarkerType.ArrowClosed, width: 14, height: 14 }
|
|
380
558
|
|
|
381
|
-
const sourceObj =
|
|
382
|
-
const targetObj =
|
|
559
|
+
const sourceObj = elementMap.get(e.source_element_id)
|
|
560
|
+
const targetObj = elementMap.get(e.target_element_id)
|
|
383
561
|
const srcTags = sourceObj?.tags || []
|
|
384
562
|
const tgtTags = targetObj?.tags || []
|
|
385
|
-
const isInactiveByLayer =
|
|
386
|
-
srcTags.some((t) =>
|
|
387
|
-
tgtTags.some((t) =>
|
|
563
|
+
const isInactiveByLayer = hiddenSet !== null && (
|
|
564
|
+
srcTags.some((t) => hiddenSet.has(t)) ||
|
|
565
|
+
tgtTags.some((t) => hiddenSet.has(t))
|
|
388
566
|
)
|
|
389
|
-
const isInactiveByFilter =
|
|
390
|
-
!srcTags.some((t) =>
|
|
391
|
-
!tgtTags.some((t) =>
|
|
567
|
+
const isInactiveByFilter = activeSet !== null && (
|
|
568
|
+
!srcTags.some((t) => activeSet.has(t)) ||
|
|
569
|
+
!tgtTags.some((t) => activeSet.has(t))
|
|
392
570
|
)
|
|
393
571
|
const isInactive = isInactiveByLayer || isInactiveByFilter
|
|
394
|
-
const isSoftFocused =
|
|
395
|
-
!
|
|
396
|
-
!
|
|
572
|
+
const isSoftFocused = hoveredSet !== null && (
|
|
573
|
+
!srcTags.some((t) => hoveredSet.has(t)) ||
|
|
574
|
+
!tgtTags.some((t) => hoveredSet.has(t))
|
|
397
575
|
)
|
|
398
576
|
const edgeOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.8
|
|
399
577
|
const markerOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 1
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
578
|
+
const newZIndex = selectedEdgeId !== null && edgeId === String(selectedEdgeId) ? 1000 : 100
|
|
579
|
+
const pointerEvents = (isInactive || isSoftFocused) ? 'none' : 'auto'
|
|
580
|
+
const labelBgOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.95
|
|
581
|
+
|
|
582
|
+
// Structural sharing: when all user-visible outputs match prev exactly, reuse prev ref.
|
|
583
|
+
// We match on the underlying connector ref plus every computed visibility/layout value.
|
|
584
|
+
if (
|
|
585
|
+
existing &&
|
|
586
|
+
existing.data &&
|
|
587
|
+
(existing.data as Connector & { __src?: unknown }).__src === e &&
|
|
588
|
+
existing.sourceHandle === layout.sourceHandle &&
|
|
589
|
+
existing.targetHandle === layout.targetHandle &&
|
|
590
|
+
existing.zIndex === newZIndex &&
|
|
591
|
+
(existing.style as CSSProperties | undefined)?.opacity === edgeOpacity &&
|
|
592
|
+
(existing.style as CSSProperties | undefined)?.pointerEvents === pointerEvents &&
|
|
593
|
+
(existing.labelStyle as CSSProperties | undefined)?.opacity === markerOpacity &&
|
|
594
|
+
(existing.labelBgStyle as CSSProperties | undefined)?.fillOpacity === labelBgOpacity &&
|
|
595
|
+
(existing.data as { sourceGroupIndex?: number }).sourceGroupIndex === layout.sourceGroupIndex &&
|
|
596
|
+
(existing.data as { targetGroupIndex?: number }).targetGroupIndex === layout.targetGroupIndex &&
|
|
597
|
+
(existing.data as { sourceGroupCount?: number }).sourceGroupCount === layout.sourceGroupCount &&
|
|
598
|
+
(existing.data as { targetGroupCount?: number }).targetGroupCount === layout.targetGroupCount
|
|
599
|
+
) {
|
|
600
|
+
return existing
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const arrowMarker = { type: MarkerType.ArrowClosed, width: 14, height: 14 }
|
|
411
604
|
|
|
412
605
|
return {
|
|
413
|
-
id:
|
|
606
|
+
id: edgeId,
|
|
414
607
|
source: String(e.source_element_id),
|
|
415
608
|
target: String(e.target_element_id),
|
|
416
|
-
sourceHandle:
|
|
417
|
-
targetHandle:
|
|
609
|
+
sourceHandle: layout.sourceHandle,
|
|
610
|
+
targetHandle: layout.targetHandle,
|
|
418
611
|
type: e.style === 'bezier' ? 'default' : (e.style || 'default'),
|
|
419
612
|
label: e.label ?? '',
|
|
420
613
|
data: {
|
|
421
614
|
...e,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
615
|
+
__src: e,
|
|
616
|
+
sourceGroupIndex: layout.sourceGroupIndex,
|
|
617
|
+
sourceGroupCount: layout.sourceGroupCount,
|
|
618
|
+
targetGroupIndex: layout.targetGroupIndex,
|
|
619
|
+
targetGroupCount: layout.targetGroupCount,
|
|
620
|
+
sourceHandleSide: layout.sourceHandleSide,
|
|
621
|
+
targetHandleSide: layout.targetHandleSide,
|
|
622
|
+
sourceHandleSlot: layout.sourceHandleSlot,
|
|
623
|
+
targetHandleSlot: layout.targetHandleSlot,
|
|
430
624
|
},
|
|
431
625
|
|
|
432
|
-
style: { stroke: 'var(--accent)', strokeWidth: 2, opacity: edgeOpacity, pointerEvents
|
|
626
|
+
style: { stroke: 'var(--accent)', strokeWidth: 2, opacity: edgeOpacity, pointerEvents },
|
|
433
627
|
labelStyle: { fontSize: 11, fill: 'var(--accent)', opacity: markerOpacity },
|
|
434
|
-
labelBgStyle: { fill: 'var(--chakra-colors-gray-900)', fillOpacity:
|
|
628
|
+
labelBgStyle: { fill: 'var(--chakra-colors-gray-900)', fillOpacity: labelBgOpacity },
|
|
435
629
|
markerEnd: (dir === 'forward' || dir === 'both') ? { ...arrowMarker, color: alphaColor('var(--accent)', markerOpacity) } : undefined,
|
|
436
630
|
markerStart: (dir === 'backward' || dir === 'both') ? { ...arrowMarker, color: alphaColor('var(--accent)', markerOpacity) } : undefined,
|
|
437
631
|
selected: existing?.selected,
|
|
438
|
-
zIndex:
|
|
632
|
+
zIndex: newZIndex,
|
|
439
633
|
}
|
|
440
|
-
})
|
|
441
|
-
)
|
|
442
|
-
}, [
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
}, [connectorLayouts, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, elementMap, setRfEdges])
|
|
443
637
|
|
|
444
638
|
|
|
445
639
|
// ── Boost z-index of selected connector ────────────────────────────────────────
|
|
446
640
|
useEffect(() => {
|
|
447
|
-
setRfEdges((prev) =>
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
641
|
+
setRfEdges((prev) => {
|
|
642
|
+
let changed = false
|
|
643
|
+
const selectedId = selectedEdgeId !== null ? String(selectedEdgeId) : null
|
|
644
|
+
const next = prev.map((edge) => {
|
|
645
|
+
const nextZIndex = selectedId !== null && edge.id === selectedId ? 1000 : 100
|
|
646
|
+
if (edge.zIndex === nextZIndex) return edge
|
|
647
|
+
changed = true
|
|
648
|
+
return { ...edge, zIndex: nextZIndex }
|
|
649
|
+
})
|
|
650
|
+
return changed ? next : prev
|
|
651
|
+
})
|
|
652
|
+
}, [selectedEdgeId, setRfEdges])
|
|
451
653
|
|
|
452
654
|
return {
|
|
453
655
|
// State
|