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