@tldiagram/core-ui 1.95.1 → 2.0.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 +184 -3
- package/dist/components/ConnectorPanel.d.ts +5 -1
- package/dist/components/CrossBranchControls.d.ts +4 -3
- package/dist/components/ElementNode.d.ts +5 -0
- package/dist/components/ElementPanel.d.ts +6 -1
- package/dist/components/LayoutSection.d.ts +2 -1
- package/dist/components/MergeDialog.d.ts +16 -0
- package/dist/components/NodeContainer.d.ts +2 -0
- package/dist/components/ProxyConnectorPanel.d.ts +4 -1
- package/dist/components/ViewExplorer/index.d.ts +1 -1
- package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
- package/dist/components/ViewFloatingMenu.d.ts +8 -1
- package/dist/components/ViewGridNode.d.ts +3 -0
- package/dist/components/ViewPanel.d.ts +2 -1
- package/dist/components/WorkspacePanel.d.ts +2 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
- package/dist/components/ZUI/focus.d.ts +32 -0
- package/dist/components/ZUI/focus.test.d.ts +1 -0
- package/dist/components/ZUI/layout.d.ts +2 -2
- package/dist/components/ZUI/proxy.d.ts +20 -4
- package/dist/components/ZUI/renderer.d.ts +35 -1
- package/dist/components/ZUI/types.d.ts +6 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
- package/dist/context/WorkspaceVersionContext.d.ts +49 -0
- package/dist/crossBranch/resolve.d.ts +39 -2
- package/dist/crossBranch/resolve.test.d.ts +1 -0
- package/dist/crossBranch/settings.d.ts +6 -1
- package/dist/crossBranch/types.d.ts +8 -0
- package/dist/hooks/useElementSearch.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16529 -14030
- package/dist/pages/InfiniteZoom.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
- package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
- package/dist/pages/viewsJumpSearch.d.ts +22 -0
- package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
- package/dist/store/useStore.d.ts +3 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/elementIcon.d.ts +2 -0
- package/dist/utils/elementIcon.test.d.ts +1 -0
- package/dist/utils/sourceEditor.d.ts +7 -0
- package/dist/utils/watchDiffSummary.d.ts +34 -0
- package/package.json +2 -2
- package/src/App.tsx +12 -8
- package/src/api/client.ts +488 -26
- package/src/components/CodePreviewPanel.tsx +90 -16
- package/src/components/ConnectorPanel.tsx +34 -3
- package/src/components/ContextNeighborElement.tsx +2 -5
- package/src/components/CrossBranchControls.tsx +46 -17
- package/src/components/ElementNode.tsx +98 -47
- package/src/components/ElementPanel.tsx +62 -25
- package/src/components/InlineElementAdder.tsx +8 -3
- package/src/components/LayoutSection.tsx +4 -1
- package/src/components/MergeDialog.tsx +269 -0
- package/src/components/NodeContainer.tsx +55 -17
- package/src/components/ProxyConnectorPanel.tsx +58 -16
- package/src/components/ViewBezierConnector.tsx +116 -21
- package/src/components/ViewExplorer/index.tsx +1 -1
- package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
- package/src/components/ViewFloatingMenu.tsx +110 -1
- package/src/components/ViewGridNode.tsx +59 -8
- package/src/components/ViewPanel.tsx +3 -2
- package/src/components/WorkspacePanel.tsx +938 -0
- package/src/components/ZUI/ZUICanvas.tsx +216 -122
- package/src/components/ZUI/focus.test.ts +534 -0
- package/src/components/ZUI/focus.ts +293 -0
- package/src/components/ZUI/layout.ts +7 -11
- package/src/components/ZUI/proxy.ts +470 -114
- package/src/components/ZUI/renderer.ts +510 -134
- package/src/components/ZUI/types.ts +6 -0
- package/src/components/ZUI/useZUIInteraction.ts +66 -29
- package/src/context/WorkspaceVersionContext.tsx +126 -0
- package/src/crossBranch/resolve.test.ts +342 -0
- package/src/crossBranch/resolve.ts +368 -68
- package/src/crossBranch/settings.ts +49 -3
- package/src/crossBranch/types.ts +9 -0
- package/src/hooks/useElementSearch.ts +45 -0
- package/src/index.css +11 -0
- package/src/index.ts +7 -0
- package/src/pages/AppearanceSettings.tsx +24 -1
- package/src/pages/Dependencies.tsx +231 -65
- package/src/pages/InfiniteZoom.tsx +41 -19
- package/src/pages/Settings.tsx +1 -1
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
- package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
- package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
- package/src/pages/ViewEditor/index.tsx +549 -59
- package/src/pages/Views.tsx +112 -41
- package/src/pages/ViewsGrid.tsx +332 -113
- package/src/pages/viewsJumpSearch.test.ts +193 -0
- package/src/pages/viewsJumpSearch.ts +111 -0
- package/src/store/useStore.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/utils/elementIcon.test.ts +28 -0
- package/src/utils/elementIcon.ts +20 -0
- package/src/utils/sourceEditor.ts +46 -0
- package/src/utils/watchDiffSummary.ts +159 -0
|
@@ -90,6 +90,18 @@ function findNearestHandleTargetInCache(targets: HandleTarget[], clientX: number
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function flattenViewTree(nodes: ViewTreeNode[]): ViewTreeNode[] {
|
|
94
|
+
const out: ViewTreeNode[] = []
|
|
95
|
+
const walk = (items: ViewTreeNode[]) => {
|
|
96
|
+
items.forEach((item) => {
|
|
97
|
+
out.push(item)
|
|
98
|
+
walk(item.children ?? [])
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
walk(nodes)
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
export function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nodes: RFNode[]) {
|
|
94
106
|
if (changes.length === 0) return nodes
|
|
95
107
|
|
|
@@ -181,6 +193,11 @@ interface CanvasInteractionOptions {
|
|
|
181
193
|
handleElementDeleted: (id: number) => void
|
|
182
194
|
handleElementPermanentlyDeleted: (id: number) => void
|
|
183
195
|
handleConnectorDeleted: (id: number) => void
|
|
196
|
+
onPlacementMoved?: (before: PlacedElement, after: PlacedElement) => void
|
|
197
|
+
onPlacementRemoved?: (placement: PlacedElement) => void
|
|
198
|
+
onConnectorUpdated?: (before: Connector, after: Connector) => void
|
|
199
|
+
onConnectorDeleted?: (connector: Connector) => void
|
|
200
|
+
onUnsupportedMutation?: () => void
|
|
184
201
|
handleUpdateTags: (elementId: number, tags: string[]) => Promise<void>
|
|
185
202
|
drawingCanvasRef: React.MutableRefObject<DrawingCanvasHandle | null>
|
|
186
203
|
snapToGrid?: boolean
|
|
@@ -259,6 +276,11 @@ export function useCanvasInteractions({
|
|
|
259
276
|
handleElementDeleted,
|
|
260
277
|
handleElementPermanentlyDeleted,
|
|
261
278
|
handleConnectorDeleted,
|
|
279
|
+
onPlacementMoved,
|
|
280
|
+
onPlacementRemoved,
|
|
281
|
+
onConnectorUpdated,
|
|
282
|
+
onConnectorDeleted,
|
|
283
|
+
onUnsupportedMutation,
|
|
262
284
|
handleUpdateTags,
|
|
263
285
|
drawingCanvasRef,
|
|
264
286
|
snapToGrid,
|
|
@@ -327,6 +349,13 @@ export function useCanvasInteractions({
|
|
|
327
349
|
handleReconnectListenersRef.current = null
|
|
328
350
|
}, [])
|
|
329
351
|
|
|
352
|
+
const clearConnectGhostListener = useCallback(() => {
|
|
353
|
+
const listener = connectGhostListenerRef.current
|
|
354
|
+
if (!listener) return
|
|
355
|
+
document.removeEventListener('mousemove', listener)
|
|
356
|
+
connectGhostListenerRef.current = null
|
|
357
|
+
}, [])
|
|
358
|
+
|
|
330
359
|
const stopHandleReconnectDrag = useCallback(() => {
|
|
331
360
|
clearHandleReconnectListeners()
|
|
332
361
|
handleReconnectDragRef.current = null
|
|
@@ -396,6 +425,7 @@ export function useCanvasInteractions({
|
|
|
396
425
|
try {
|
|
397
426
|
const obj = await api.elements.create({ name, kind: '' })
|
|
398
427
|
await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
|
|
428
|
+
onUnsupportedMutation?.()
|
|
399
429
|
await refreshElements()
|
|
400
430
|
const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
|
|
401
431
|
if (placed) upsertPlacementGraphSnapshot(viewId, placed)
|
|
@@ -410,9 +440,10 @@ export function useCanvasInteractions({
|
|
|
410
440
|
})
|
|
411
441
|
const connector = connectorToConnector(newConnector)
|
|
412
442
|
await finalizeConnectorCreate(connector)
|
|
443
|
+
onUnsupportedMutation?.()
|
|
413
444
|
}
|
|
414
445
|
} catch { /* intentionally empty */ }
|
|
415
|
-
}, [addingElementAt, canEdit, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
|
|
446
|
+
}, [addingElementAt, canEdit, finalizeConnectorCreate, onUnsupportedMutation, refreshElements, rfNodesRef, viewId, viewElementsRef])
|
|
416
447
|
|
|
417
448
|
const handleConfirmExistingElement = useCallback(async (obj: LibraryElement) => {
|
|
418
449
|
if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'add') return
|
|
@@ -425,6 +456,7 @@ export function useCanvasInteractions({
|
|
|
425
456
|
try {
|
|
426
457
|
if (!existingElementIds.has(obj.id)) {
|
|
427
458
|
await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
|
|
459
|
+
onUnsupportedMutation?.()
|
|
428
460
|
await refreshElements()
|
|
429
461
|
const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
|
|
430
462
|
if (placed) upsertPlacementGraphSnapshot(viewId, placed)
|
|
@@ -443,9 +475,10 @@ export function useCanvasInteractions({
|
|
|
443
475
|
})
|
|
444
476
|
const connector = connectorToConnector(newConnector)
|
|
445
477
|
await finalizeConnectorCreate(connector)
|
|
478
|
+
onUnsupportedMutation?.()
|
|
446
479
|
}
|
|
447
480
|
} catch { /* intentionally empty */ }
|
|
448
|
-
}, [addingElementAt, canEdit, existingElementIds, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
|
|
481
|
+
}, [addingElementAt, canEdit, existingElementIds, finalizeConnectorCreate, onUnsupportedMutation, refreshElements, rfNodesRef, viewId, viewElementsRef])
|
|
449
482
|
|
|
450
483
|
const handleConfirmConnectExistingElement = useCallback(async (obj: LibraryElement) => {
|
|
451
484
|
if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'connect') return
|
|
@@ -470,13 +503,21 @@ export function useCanvasInteractions({
|
|
|
470
503
|
})
|
|
471
504
|
const connector = connectorToConnector(newConnector)
|
|
472
505
|
await finalizeConnectorCreate(connector)
|
|
506
|
+
onUnsupportedMutation?.()
|
|
473
507
|
} catch { /* intentionally empty */ }
|
|
474
|
-
}, [addingElementAt, canEdit, finalizeConnectorCreate, rfNodesRef, viewId])
|
|
508
|
+
}, [addingElementAt, canEdit, finalizeConnectorCreate, onUnsupportedMutation, rfNodesRef, viewId])
|
|
475
509
|
|
|
476
510
|
// ── Zoom-in / zoom-out stable callbacks ───────────────────────────────────
|
|
477
511
|
const stableOnZoomIn = useCallback(async (elementId: number) => {
|
|
478
512
|
const childLinks = linksMapRef.current[elementId] || []
|
|
479
|
-
if (childLinks.length > 0) {
|
|
513
|
+
if (childLinks.length > 0) {
|
|
514
|
+
setSelectedElement(null)
|
|
515
|
+
setSelectedEdge(null)
|
|
516
|
+
closeElementPanel()
|
|
517
|
+
closeConnectorPanel()
|
|
518
|
+
navigateRef.current(`/views/${childLinks[0].to_view_id}`)
|
|
519
|
+
return
|
|
520
|
+
}
|
|
480
521
|
|
|
481
522
|
const obj = viewElementsRef.current.find((o) => o.element_id === elementId)
|
|
482
523
|
if (obj?.has_view) {
|
|
@@ -491,6 +532,10 @@ export function useCanvasInteractions({
|
|
|
491
532
|
}
|
|
492
533
|
const existingView = findInTree(treeDataRef.current)
|
|
493
534
|
if (existingView) {
|
|
535
|
+
setSelectedElement(null)
|
|
536
|
+
setSelectedEdge(null)
|
|
537
|
+
closeElementPanel()
|
|
538
|
+
closeConnectorPanel()
|
|
494
539
|
navigateRef.current(`/views/${existingView.id}`)
|
|
495
540
|
return
|
|
496
541
|
}
|
|
@@ -506,9 +551,13 @@ export function useCanvasInteractions({
|
|
|
506
551
|
[elementId]: [...(prev[elementId] || []),
|
|
507
552
|
{ id: 0, element_id: elementId, from_view_id: cid, to_view_id: newView.id, to_view_name: newView.name, relation_type: 'child' as const }],
|
|
508
553
|
}))
|
|
554
|
+
setSelectedElement(null)
|
|
555
|
+
setSelectedEdge(null)
|
|
556
|
+
closeElementPanel()
|
|
557
|
+
closeConnectorPanel()
|
|
509
558
|
navigateRef.current(`/views/${newView.id}`)
|
|
510
559
|
} catch { /* intentionally empty */ }
|
|
511
|
-
}, [canEdit, linksMapRef, viewIdRef, viewElementsRef, navigateRef, setLinksMap, treeDataRef])
|
|
560
|
+
}, [canEdit, linksMapRef, viewIdRef, viewElementsRef, navigateRef, setLinksMap, treeDataRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
|
|
512
561
|
|
|
513
562
|
const stableOnZoomOut = useCallback(async (elementId: number) => {
|
|
514
563
|
const parentLinks = parentLinksMapRef.current[elementId] || []
|
|
@@ -517,7 +566,14 @@ export function useCanvasInteractions({
|
|
|
517
566
|
// from the clicked element's ID for elements like functions/classes that
|
|
518
567
|
// don't own a view themselves).
|
|
519
568
|
const anyParentLink = parentLinks[0] ?? Object.values(parentLinksMapRef.current).flat()[0]
|
|
520
|
-
if (anyParentLink) {
|
|
569
|
+
if (anyParentLink) {
|
|
570
|
+
setSelectedElement(null)
|
|
571
|
+
setSelectedEdge(null)
|
|
572
|
+
closeElementPanel()
|
|
573
|
+
closeConnectorPanel()
|
|
574
|
+
navigateRef.current(`/views/${anyParentLink.from_view_id}`)
|
|
575
|
+
return
|
|
576
|
+
}
|
|
521
577
|
|
|
522
578
|
// Final fallback: use current view's parent_view_id if available
|
|
523
579
|
const findInTreeById = (nodes: ViewTreeNode[], id: number): ViewTreeNode | null => {
|
|
@@ -530,13 +586,21 @@ export function useCanvasInteractions({
|
|
|
530
586
|
}
|
|
531
587
|
const currentView = findInTreeById(treeDataRef.current, viewIdRef.current || -1)
|
|
532
588
|
if (currentView?.parent_view_id) {
|
|
589
|
+
setSelectedElement(null)
|
|
590
|
+
setSelectedEdge(null)
|
|
591
|
+
closeElementPanel()
|
|
592
|
+
closeConnectorPanel()
|
|
533
593
|
navigateRef.current(`/views/${currentView.parent_view_id}`)
|
|
534
594
|
}
|
|
535
|
-
}, [parentLinksMapRef, navigateRef, treeDataRef, viewIdRef])
|
|
595
|
+
}, [parentLinksMapRef, navigateRef, treeDataRef, viewIdRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
|
|
536
596
|
|
|
537
597
|
const stableOnNavigateToView = useCallback((id: number) => {
|
|
598
|
+
setSelectedElement(null)
|
|
599
|
+
setSelectedEdge(null)
|
|
600
|
+
closeElementPanel()
|
|
601
|
+
closeConnectorPanel()
|
|
538
602
|
navigateRef.current(`/views/${id}`)
|
|
539
|
-
}, [navigateRef])
|
|
603
|
+
}, [navigateRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
|
|
540
604
|
|
|
541
605
|
const stableOnHoverZoom = useCallback((elementId: number, type: 'in' | 'out' | null) => {
|
|
542
606
|
const prev = hoveredZoomRef.current
|
|
@@ -555,15 +619,17 @@ export function useCanvasInteractions({
|
|
|
555
619
|
|
|
556
620
|
const stableOnRemoveElement = useCallback(async (elementId: number) => {
|
|
557
621
|
if (!canEdit || viewId === null) return
|
|
622
|
+
const removedPlacement = viewElementsRef.current.find((element) => element.element_id === elementId) ?? null
|
|
558
623
|
try {
|
|
559
624
|
await api.workspace.views.placements.remove(viewId, elementId)
|
|
560
625
|
removePlacementGraphSnapshot(viewId, elementId)
|
|
561
626
|
removeElementPlacement(elementId)
|
|
627
|
+
if (removedPlacement) onPlacementRemoved?.(removedPlacement)
|
|
562
628
|
handleElementDeleted(elementId)
|
|
563
629
|
setInteractionSourceId(null)
|
|
564
630
|
pendingConnectionSourceHandleRef.current = null
|
|
565
631
|
} catch { /* intentionally empty */ }
|
|
566
|
-
}, [canEdit, viewId, removeElementPlacement, handleElementDeleted])
|
|
632
|
+
}, [canEdit, viewId, viewElementsRef, removeElementPlacement, onPlacementRemoved, handleElementDeleted])
|
|
567
633
|
|
|
568
634
|
const connectClickModeToHandle = useCallback(async (targetElementId: number, targetHandle: string) => {
|
|
569
635
|
if (!canEdit) return
|
|
@@ -594,8 +660,9 @@ export function useCanvasInteractions({
|
|
|
594
660
|
})
|
|
595
661
|
const connector = connectorToConnector(newConnector)
|
|
596
662
|
await finalizeConnectorCreate(connector)
|
|
663
|
+
onUnsupportedMutation?.()
|
|
597
664
|
} catch { /* intentionally empty */ }
|
|
598
|
-
}, [canEdit, finalizeConnectorCreate, interactionSourceIdRef, viewIdRef])
|
|
665
|
+
}, [canEdit, finalizeConnectorCreate, interactionSourceIdRef, onUnsupportedMutation, viewIdRef])
|
|
599
666
|
|
|
600
667
|
const stableOnInteractionStart = useCallback((elementId: number, options?: InteractionStartOptions) => {
|
|
601
668
|
if (!canEdit) return
|
|
@@ -684,15 +751,20 @@ export function useCanvasInteractions({
|
|
|
684
751
|
return
|
|
685
752
|
}
|
|
686
753
|
|
|
754
|
+
const beforePlacement = currentObj ? { ...currentObj, position_x: startPos?.x ?? currentObj.position_x, position_y: startPos?.y ?? currentObj.position_y } : null
|
|
755
|
+
const afterPlacement = currentObj ? { ...currentObj, position_x: node.position.x, position_y: node.position.y } : null
|
|
687
756
|
updateElementPosition(elementId, node.position.x, node.position.y)
|
|
688
757
|
clearTimeout(positionTimers.current[node.id])
|
|
689
758
|
positionTimers.current[node.id] = setTimeout(() => {
|
|
690
759
|
api.workspace.views.placements
|
|
691
760
|
.updatePosition(viewId, elementId, node.position.x, node.position.y)
|
|
761
|
+
.then(() => {
|
|
762
|
+
if (beforePlacement && afterPlacement) onPlacementMoved?.(beforePlacement, afterPlacement)
|
|
763
|
+
})
|
|
692
764
|
.catch(() => { /* intentionally empty */ })
|
|
693
765
|
}, 400)
|
|
694
766
|
delete dragStartPositionsRef.current[node.id]
|
|
695
|
-
}, [canEdit, updateElementPosition, viewId, viewElementsRef])
|
|
767
|
+
}, [canEdit, updateElementPosition, viewId, viewElementsRef, onPlacementMoved])
|
|
696
768
|
|
|
697
769
|
// ── Connections ────────────────────────────────────────────────────────────
|
|
698
770
|
const onConnect: OnConnect = useCallback(async (params: Connection) => {
|
|
@@ -712,11 +784,13 @@ export function useCanvasInteractions({
|
|
|
712
784
|
})
|
|
713
785
|
const connector = connectorToConnector(newConnector)
|
|
714
786
|
await finalizeConnectorCreate(connector)
|
|
787
|
+
onUnsupportedMutation?.()
|
|
715
788
|
} catch { /* intentionally empty */ }
|
|
716
|
-
}, [canEdit, finalizeConnectorCreate, viewId])
|
|
789
|
+
}, [canEdit, finalizeConnectorCreate, onUnsupportedMutation, viewId])
|
|
717
790
|
|
|
718
791
|
const onConnectStart = useCallback((_: React.MouseEvent | React.TouchEvent, { nodeId }: OnConnectStartParams) => {
|
|
719
792
|
if (!canEdit || isReconnectingRef.current) return
|
|
793
|
+
clearConnectGhostListener()
|
|
720
794
|
connectingSourceRef.current = nodeId
|
|
721
795
|
connectWasValidRef.current = false
|
|
722
796
|
const handleTargets = collectHandleTargets(nodeId ?? undefined)
|
|
@@ -730,13 +804,10 @@ export function useCanvasInteractions({
|
|
|
730
804
|
}
|
|
731
805
|
connectGhostListenerRef.current = listener
|
|
732
806
|
document.addEventListener('mousemove', listener)
|
|
733
|
-
}, [canEdit])
|
|
807
|
+
}, [canEdit, clearConnectGhostListener])
|
|
734
808
|
|
|
735
809
|
const onConnectEnd = useCallback((event: MouseEvent | TouchEvent) => {
|
|
736
|
-
|
|
737
|
-
document.removeEventListener('mousemove', connectGhostListenerRef.current)
|
|
738
|
-
connectGhostListenerRef.current = null
|
|
739
|
-
}
|
|
810
|
+
clearConnectGhostListener()
|
|
740
811
|
setConnectGhostPos(null)
|
|
741
812
|
if (!canEdit || isReconnectingRef.current) return
|
|
742
813
|
const sourceId = connectingSourceRef.current
|
|
@@ -774,13 +845,14 @@ export function useCanvasInteractions({
|
|
|
774
845
|
}).then((connector) => {
|
|
775
846
|
const next = connectorToConnector(connector)
|
|
776
847
|
void finalizeConnectorCreate(next)
|
|
848
|
+
onUnsupportedMutation?.()
|
|
777
849
|
}).catch(() => { /* intentionally empty */ })
|
|
778
850
|
} else {
|
|
779
851
|
setPendingConnectionSource(sourceElementId)
|
|
780
852
|
suppressNextPaneClickRef.current = true
|
|
781
853
|
showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
|
|
782
854
|
}
|
|
783
|
-
}, [canEdit, finalizeConnectorCreate, showAddingElementAt, rfNodesRef, viewIdRef])
|
|
855
|
+
}, [canEdit, clearConnectGhostListener, finalizeConnectorCreate, onUnsupportedMutation, showAddingElementAt, rfNodesRef, viewIdRef])
|
|
784
856
|
|
|
785
857
|
// ── Reconnect ──────────────────────────────────────────────────────────────
|
|
786
858
|
const performReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
|
|
@@ -806,8 +878,9 @@ export function useCanvasInteractions({
|
|
|
806
878
|
const connector = connectorToConnector(updated)
|
|
807
879
|
upsertConnectorGraphSnapshot(connector)
|
|
808
880
|
replaceConnector(connector)
|
|
881
|
+
if (existingData) onConnectorUpdated?.(existingData, connector)
|
|
809
882
|
} catch { /* intentionally empty */ }
|
|
810
|
-
}, [canEdit, replaceConnector, viewId, setRfEdges])
|
|
883
|
+
}, [canEdit, replaceConnector, viewId, setRfEdges, onConnectorUpdated])
|
|
811
884
|
const onReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
|
|
812
885
|
await performReconnect(oldConnector, newConnection)
|
|
813
886
|
}, [performReconnect])
|
|
@@ -978,8 +1051,9 @@ export function useCanvasInteractions({
|
|
|
978
1051
|
}, [interactionSourceId])
|
|
979
1052
|
|
|
980
1053
|
useEffect(() => () => {
|
|
1054
|
+
clearConnectGhostListener()
|
|
981
1055
|
stopHandleReconnectDrag()
|
|
982
|
-
}, [stopHandleReconnectDrag])
|
|
1056
|
+
}, [clearConnectGhostListener, stopHandleReconnectDrag])
|
|
983
1057
|
|
|
984
1058
|
// ── Connector interactions ─────────────────────────────────────────────────────
|
|
985
1059
|
const onEdgeContextMenu = useCallback((e: React.MouseEvent, rfConnector: RFEdge) => {
|
|
@@ -1012,7 +1086,7 @@ export function useCanvasInteractions({
|
|
|
1012
1086
|
setSelectedProxyConnectorDetails(null)
|
|
1013
1087
|
setSelectedEdge(connector)
|
|
1014
1088
|
openConnectorPanelRef.current()
|
|
1015
|
-
}, [closeElementPanel, connectors, openProxyConnectorPanel, setSelectedEdge, setSelectedElement, setSelectedProxyConnectorDetails])
|
|
1089
|
+
}, [closeConnectorPanel, closeElementPanel, connectors, openProxyConnectorPanel, setSelectedEdge, setSelectedElement, setSelectedProxyConnectorDetails])
|
|
1016
1090
|
|
|
1017
1091
|
// ── Pane interactions ─────────────────────────────────────────────────────
|
|
1018
1092
|
const onPaneClick = useCallback((e: React.MouseEvent) => {
|
|
@@ -1221,7 +1295,11 @@ export function useCanvasInteractions({
|
|
|
1221
1295
|
} else {
|
|
1222
1296
|
const connectorId = getConnectorDeletionTarget(selectedConnector)
|
|
1223
1297
|
if (connectorId === null) return
|
|
1298
|
+
const deletedConnector = selectedConnector?.id === connectorId
|
|
1299
|
+
? selectedConnector
|
|
1300
|
+
: connectors.find((connector) => connector.id === connectorId) ?? null
|
|
1224
1301
|
api.workspace.connectors.delete('', connectorId).then(() => {
|
|
1302
|
+
if (deletedConnector) onConnectorDeleted?.(deletedConnector)
|
|
1225
1303
|
handleConnectorDeleted(connectorId)
|
|
1226
1304
|
setSelectedEdge(null)
|
|
1227
1305
|
closeConnectorPanel()
|
|
@@ -1265,7 +1343,7 @@ export function useCanvasInteractions({
|
|
|
1265
1343
|
const cid = viewIdRef.current
|
|
1266
1344
|
if (!cid) return
|
|
1267
1345
|
const incoming = incomingLinksRef.current
|
|
1268
|
-
const tree = treeDataRef.current
|
|
1346
|
+
const tree = flattenViewTree(treeDataRef.current)
|
|
1269
1347
|
const nav = navigateRef.current
|
|
1270
1348
|
const links = linksMapRef.current
|
|
1271
1349
|
const treeNode = tree.find((n) => n.id === cid)
|
|
@@ -1332,7 +1410,7 @@ export function useCanvasInteractions({
|
|
|
1332
1410
|
}
|
|
1333
1411
|
window.addEventListener('keydown', handler)
|
|
1334
1412
|
return () => window.removeEventListener('keydown', handler)
|
|
1335
|
-
}, [canEdit, refreshGrid, selectedElement, selectedConnector, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted,
|
|
1413
|
+
}, [canEdit, refreshGrid, selectedElement, selectedConnector, connectors, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted, onConnectorDeleted, closeElementPanel, closeConnectorPanel, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, rfNodesRef, viewElementsRef, setLinksMap, showAddingElementAt, setSelectedElement, setSelectedEdge, containerRef, linksMapRef])
|
|
1336
1414
|
|
|
1337
1415
|
// ── DnD handlers ──────────────────────────────────────────────────────────
|
|
1338
1416
|
const onDragOver = useCallback((e: React.DragEvent) => {
|
|
@@ -1351,6 +1429,7 @@ export function useCanvasInteractions({
|
|
|
1351
1429
|
const pos = screenToFlowPositionRef.current({ x: e.clientX, y: e.clientY })
|
|
1352
1430
|
try {
|
|
1353
1431
|
await api.workspace.views.placements.add(viewId, obj.id, pos.x - 100, pos.y - 40)
|
|
1432
|
+
onUnsupportedMutation?.()
|
|
1354
1433
|
await refreshElements()
|
|
1355
1434
|
const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
|
|
1356
1435
|
if (placed) upsertPlacementGraphSnapshot(viewId, placed)
|
|
@@ -1402,7 +1481,7 @@ export function useCanvasInteractions({
|
|
|
1402
1481
|
}
|
|
1403
1482
|
}
|
|
1404
1483
|
}
|
|
1405
|
-
}, [canEdit, viewId, existingElementIds, refreshElements, rfNodesRef, viewElementsRef, layers, handleUpdateTags])
|
|
1484
|
+
}, [canEdit, viewId, existingElementIds, onUnsupportedMutation, refreshElements, rfNodesRef, viewElementsRef, layers, handleUpdateTags])
|
|
1406
1485
|
|
|
1407
1486
|
const onWheelCapture = useCallback((e: React.WheelEvent) => {
|
|
1408
1487
|
if (touchStateRef.current.touches.size === 2) return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { type Edge as RFEdge, type Node as RFNode } from 'reactflow'
|
|
3
|
-
import type { PlacedElement } from '../../../types'
|
|
3
|
+
import type { Connector, PlacedElement } from '../../../types'
|
|
4
4
|
import type { CrossBranchContextSettings, ProxyConnectorDetails, WorkspaceGraphSnapshot } from '../../../crossBranch/types'
|
|
5
5
|
import { resolveViewProxyGraph } from '../../../crossBranch/resolve'
|
|
6
6
|
|
|
@@ -69,6 +69,61 @@ function isAncestorContextNode(
|
|
|
69
69
|
(snapshot.descendantsByViewId[ownedViewId]?.includes(descendant.placementViewId) ?? false)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function canonicalElementPairKey(leftId: number, rightId: number) {
|
|
73
|
+
return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function canonicalNodePairKey(leftId: string, rightId: string) {
|
|
77
|
+
return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildDirectConnectorPairSet(connectors: Connector[], visibleElementIds: Set<number>) {
|
|
81
|
+
const pairs = new Set<string>()
|
|
82
|
+
for (const connector of connectors) {
|
|
83
|
+
if (!visibleElementIds.has(connector.source_element_id) || !visibleElementIds.has(connector.target_element_id)) continue
|
|
84
|
+
pairs.add(canonicalElementPairKey(connector.source_element_id, connector.target_element_id))
|
|
85
|
+
}
|
|
86
|
+
return pairs
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mergeHiddenProxyDetails(
|
|
90
|
+
existing: ProxyConnectorDetails | undefined,
|
|
91
|
+
next: ProxyConnectorDetails,
|
|
92
|
+
): ProxyConnectorDetails {
|
|
93
|
+
if (!existing) {
|
|
94
|
+
return {
|
|
95
|
+
...next,
|
|
96
|
+
ownerViewIds: [...next.ownerViewIds],
|
|
97
|
+
ownerViewNames: [...next.ownerViewNames],
|
|
98
|
+
connectors: [...next.connectors],
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ownerViews = new Map<number, string>()
|
|
103
|
+
existing.ownerViewIds.forEach((ownerViewId, index) => {
|
|
104
|
+
ownerViews.set(ownerViewId, existing.ownerViewNames[index] ?? `View ${ownerViewId}`)
|
|
105
|
+
})
|
|
106
|
+
next.ownerViewIds.forEach((ownerViewId, index) => {
|
|
107
|
+
ownerViews.set(ownerViewId, next.ownerViewNames[index] ?? `View ${ownerViewId}`)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const connectors = [...existing.connectors, ...next.connectors]
|
|
111
|
+
const count = connectors.length
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
key: existing.key,
|
|
115
|
+
label: count === 1 ? connectors[0]?.connector.label?.trim() || connectors[0]?.connector.relationship?.trim() || 'Cross-branch' : `${count} connectors`,
|
|
116
|
+
count,
|
|
117
|
+
sourceAnchorId: existing.sourceAnchorId,
|
|
118
|
+
targetAnchorId: existing.targetAnchorId,
|
|
119
|
+
sourceAnchorName: existing.sourceAnchorName,
|
|
120
|
+
targetAnchorName: existing.targetAnchorName,
|
|
121
|
+
ownerViewIds: Array.from(ownerViews.keys()),
|
|
122
|
+
ownerViewNames: Array.from(ownerViews.values()),
|
|
123
|
+
connectors,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
72
127
|
export function useViewContextNeighbours({
|
|
73
128
|
snapshot,
|
|
74
129
|
settings,
|
|
@@ -82,18 +137,38 @@ export function useViewContextNeighbours({
|
|
|
82
137
|
}: Props) {
|
|
83
138
|
return useMemo(() => {
|
|
84
139
|
if (!snapshot || viewId == null || !settings.enabled) {
|
|
85
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
contextNodes: [] as RFNode[],
|
|
142
|
+
contextConnectors: [] as RFEdge[],
|
|
143
|
+
proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails>,
|
|
144
|
+
hiddenProxyCountsByPair: {} as Record<string, number>,
|
|
145
|
+
hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
|
|
146
|
+
}
|
|
86
147
|
}
|
|
87
148
|
|
|
88
149
|
const { proxyNodes, proxyConnectors, proxyConnectorDetailsByKey } = resolveViewProxyGraph(snapshot, viewId, viewElements, settings)
|
|
89
150
|
if (proxyNodes.length === 0 && proxyConnectors.length === 0) {
|
|
90
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
contextNodes: [] as RFNode[],
|
|
153
|
+
contextConnectors: [] as RFEdge[],
|
|
154
|
+
proxyConnectorDetailsByKey,
|
|
155
|
+
hiddenProxyCountsByPair: {} as Record<string, number>,
|
|
156
|
+
hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
|
|
157
|
+
}
|
|
91
158
|
}
|
|
92
159
|
|
|
93
160
|
const mainNodes = rfNodes.filter((node) => node.type === 'elementNode')
|
|
94
161
|
if (mainNodes.length === 0) {
|
|
95
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
contextNodes: [] as RFNode[],
|
|
164
|
+
contextConnectors: [] as RFEdge[],
|
|
165
|
+
proxyConnectorDetailsByKey,
|
|
166
|
+
hiddenProxyCountsByPair: {} as Record<string, number>,
|
|
167
|
+
hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
|
|
168
|
+
}
|
|
96
169
|
}
|
|
170
|
+
const visibleElementIds = new Set(viewElements.map((element) => element.element_id))
|
|
171
|
+
const directConnectorPairs = buildDirectConnectorPairSet(snapshot.connectorsByViewId[viewId] ?? [], visibleElementIds)
|
|
97
172
|
|
|
98
173
|
let minX = Infinity
|
|
99
174
|
let minY = Infinity
|
|
@@ -460,6 +535,8 @@ export function useViewContextNeighbours({
|
|
|
460
535
|
})
|
|
461
536
|
|
|
462
537
|
const seenCollapsedPairs = new Set<string>()
|
|
538
|
+
const hiddenProxyCountsByPair: Record<string, number> = {}
|
|
539
|
+
const hiddenProxyDetailsByPair: Record<string, ProxyConnectorDetails> = {}
|
|
463
540
|
const contextConnectors: RFEdge[] = proxyConnectors.flatMap((connector) => {
|
|
464
541
|
let sourceId = connector.sourceAnchorId
|
|
465
542
|
let targetId = connector.targetAnchorId
|
|
@@ -472,7 +549,20 @@ export function useViewContextNeighbours({
|
|
|
472
549
|
|
|
473
550
|
if (sourceId === targetId) return []
|
|
474
551
|
|
|
475
|
-
const pairKey =
|
|
552
|
+
const pairKey = canonicalNodePairKey(sourceId, targetId)
|
|
553
|
+
if (directConnectorPairs.has(pairKey)) {
|
|
554
|
+
hiddenProxyCountsByPair[pairKey] = (hiddenProxyCountsByPair[pairKey] ?? 0) + connector.details.count
|
|
555
|
+
hiddenProxyDetailsByPair[pairKey] = mergeHiddenProxyDetails(
|
|
556
|
+
hiddenProxyDetailsByPair[pairKey],
|
|
557
|
+
{
|
|
558
|
+
...connector.details,
|
|
559
|
+
key: `hidden:${pairKey}`,
|
|
560
|
+
sourceAnchorId: sourceId,
|
|
561
|
+
targetAnchorId: targetId,
|
|
562
|
+
},
|
|
563
|
+
)
|
|
564
|
+
return []
|
|
565
|
+
}
|
|
476
566
|
if (seenCollapsedPairs.has(pairKey)) return []
|
|
477
567
|
seenCollapsedPairs.add(pairKey)
|
|
478
568
|
|
|
@@ -496,6 +586,12 @@ export function useViewContextNeighbours({
|
|
|
496
586
|
}]
|
|
497
587
|
})
|
|
498
588
|
|
|
499
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
contextNodes: [ContextBoundaryElement, ...contextNodes],
|
|
591
|
+
contextConnectors,
|
|
592
|
+
proxyConnectorDetailsByKey,
|
|
593
|
+
hiddenProxyCountsByPair,
|
|
594
|
+
hiddenProxyDetailsByPair,
|
|
595
|
+
}
|
|
500
596
|
}, [snapshot, settings, viewId, viewElements, rfNodes, stableOnNavigateToView, onSelectProxyDetails, expandedAncestorGroups, onToggleAncestorGroup])
|
|
501
597
|
}
|