@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,6 +1,7 @@
|
|
|
1
1
|
import type { DrawingCanvasHandle } from '../../../components/DrawingCanvas';
|
|
2
2
|
import { type Connection, type Edge as RFEdge, type EdgeChange, type Node as RFNode, type NodeChange, type NodeDragHandler, type OnConnect, type OnConnectStartParams } from 'reactflow';
|
|
3
3
|
import type { Connector, PlacedElement, ViewTreeNode, LibraryElement, ViewLayer, ViewConnector, IncomingViewConnector } from '../../../types';
|
|
4
|
+
export declare function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nodes: RFNode[]): RFNode[];
|
|
4
5
|
interface CanvasInteractionOptions {
|
|
5
6
|
viewId: number | null;
|
|
6
7
|
canEdit: boolean;
|
|
@@ -79,7 +80,7 @@ type HandleReconnectDragState = {
|
|
|
79
80
|
hoveredNodeId?: string;
|
|
80
81
|
hoveredHandleId?: string;
|
|
81
82
|
};
|
|
82
|
-
export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _drawingMode, isMobileLayout: _isMobileLayout, rfNodesRef, rfEdgesRef: _rfEdgesRef, viewElementsRef, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, containerRef, interactionSourceIdRef, hoveredZoomRef, hoverPanLockedUntilRef, setViewElements, setConnectors, setRfNodes, setRfEdges, setLinksMap, setParentLinksMap: _setParentLinksMap, setHoveredZoom, refreshGrid, refreshElements, stableOnConnectTo, existingElementIds, linksMapRef, parentLinksMapRef, openElementPanel: _openElementPanel, closeElementPanel: closeElementPanel, openConnectorPanel: openConnectorPanel, closeConnectorPanel: closeConnectorPanel, selectedElement, selectedEdgeId, connectors, layers, setSelectedElement, setSelectedEdge, setSelectedEdgeId, setSelectedProxyConnectorDetails, openProxyConnectorPanel, closeProxyConnectorPanel, handleElementDeleted, handleElementPermanentlyDeleted, handleConnectorDeleted, handleUpdateTags, drawingCanvasRef, snapToGrid, onMoveStateChange, }: CanvasInteractionOptions): {
|
|
83
|
+
export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _drawingMode, isMobileLayout: _isMobileLayout, rfNodesRef, rfEdgesRef: _rfEdgesRef, viewElementsRef, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, containerRef, interactionSourceIdRef, hoveredZoomRef, hoverPanLockedUntilRef, setViewElements: _setViewElements, setConnectors: _setConnectors, setRfNodes, setRfEdges, setLinksMap, setParentLinksMap: _setParentLinksMap, setHoveredZoom, refreshGrid, refreshElements, stableOnConnectTo, existingElementIds, linksMapRef, parentLinksMapRef, openElementPanel: _openElementPanel, closeElementPanel: closeElementPanel, openConnectorPanel: openConnectorPanel, closeConnectorPanel: closeConnectorPanel, selectedElement, selectedEdgeId, connectors, layers, setSelectedElement, setSelectedEdge, setSelectedEdgeId, setSelectedProxyConnectorDetails, openProxyConnectorPanel, closeProxyConnectorPanel, handleElementDeleted, handleElementPermanentlyDeleted, handleConnectorDeleted, handleUpdateTags, drawingCanvasRef, snapToGrid, onMoveStateChange, }: CanvasInteractionOptions): {
|
|
83
84
|
canvasMenu: {
|
|
84
85
|
x: number;
|
|
85
86
|
y: number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ViewTreeNode, PlacedElement, LibraryElement, Connector, IncomingViewConnector, ViewConnector, Tag } from '../../../types';
|
|
1
|
+
import type { ViewTreeNode, PlacedElement, LibraryElement, Connector, Tag } from '../../../types';
|
|
3
2
|
interface ViewDataOptions {
|
|
4
3
|
viewId: number | null;
|
|
5
4
|
interactionSourceId: number | null;
|
|
@@ -37,38 +36,38 @@ interface ViewDataOptions {
|
|
|
37
36
|
}
|
|
38
37
|
export declare function useViewData({ viewId, interactionSourceId, clickConnectMode, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, hoveredLayerColor, tagColors, stableOnZoomIn, stableOnZoomOut, stableOnNavigateToView, stableOnSelect, stableOnOpenCodePreview, stableOnInteractionStart, stableOnConnectTo, stableOnStartHandleReconnect, stableOnRemoveElement, stableOnHoverZoom, hoveredZoomRef, }: ViewDataOptions): {
|
|
39
38
|
view: ViewTreeNode | null | undefined;
|
|
40
|
-
setView:
|
|
39
|
+
setView: (view: ViewTreeNode | null | undefined) => void;
|
|
41
40
|
viewElements: PlacedElement[];
|
|
42
|
-
setViewElements:
|
|
41
|
+
setViewElements: (next: import("../../../store/useStore").StoreSetter<PlacedElement[]>) => void;
|
|
43
42
|
connectors: Connector[];
|
|
44
|
-
setConnectors:
|
|
45
|
-
rfNodes:
|
|
46
|
-
setRfNodes: import("
|
|
47
|
-
rfEdges:
|
|
48
|
-
setRfEdges: import("
|
|
49
|
-
linksMap: Record<number, ViewConnector[]>;
|
|
50
|
-
setLinksMap: import("
|
|
51
|
-
parentLinksMap: Record<number, ViewConnector[]>;
|
|
52
|
-
setParentLinksMap: import("
|
|
53
|
-
incomingLinks: IncomingViewConnector[];
|
|
43
|
+
setConnectors: (next: import("../../../store/useStore").StoreSetter<Connector[]>) => void;
|
|
44
|
+
rfNodes: import("reactflow").Node[];
|
|
45
|
+
setRfNodes: (next: import("../../../store/useStore").StoreSetter<import("reactflow").Node[]>) => void;
|
|
46
|
+
rfEdges: import("reactflow").Edge[];
|
|
47
|
+
setRfEdges: (next: import("../../../store/useStore").StoreSetter<import("reactflow").Edge[]>) => void;
|
|
48
|
+
linksMap: Record<number, import("../../..").ViewConnector[]>;
|
|
49
|
+
setLinksMap: (next: import("../../../store/useStore").StoreSetter<Record<number, import("../../..").ViewConnector[]>>) => void;
|
|
50
|
+
parentLinksMap: Record<number, import("../../..").ViewConnector[]>;
|
|
51
|
+
setParentLinksMap: (next: import("../../../store/useStore").StoreSetter<Record<number, import("../../..").ViewConnector[]>>) => void;
|
|
52
|
+
incomingLinks: import("../../..").IncomingViewConnector[];
|
|
54
53
|
treeData: ViewTreeNode[];
|
|
55
54
|
allElements: LibraryElement[];
|
|
56
55
|
libraryRefresh: number;
|
|
57
|
-
setLibraryRefresh:
|
|
56
|
+
setLibraryRefresh: (next: import("../../../store/useStore").StoreSetter<number>) => void;
|
|
58
57
|
existingElementIds: Set<number>;
|
|
59
58
|
viewElementsRef: import("react").MutableRefObject<PlacedElement[]>;
|
|
60
|
-
linksMapRef: import("react").MutableRefObject<Record<number, ViewConnector[]>>;
|
|
61
|
-
parentLinksMapRef: import("react").MutableRefObject<Record<number, ViewConnector[]>>;
|
|
62
|
-
incomingLinksRef: import("react").MutableRefObject<IncomingViewConnector[]>;
|
|
59
|
+
linksMapRef: import("react").MutableRefObject<Record<number, import("../../..").ViewConnector[]>>;
|
|
60
|
+
parentLinksMapRef: import("react").MutableRefObject<Record<number, import("../../..").ViewConnector[]>>;
|
|
61
|
+
incomingLinksRef: import("react").MutableRefObject<import("../../..").IncomingViewConnector[]>;
|
|
63
62
|
treeDataRef: import("react").MutableRefObject<ViewTreeNode[]>;
|
|
64
|
-
rfNodesRef: import("react").MutableRefObject<
|
|
65
|
-
rfEdgesRef: import("react").MutableRefObject<
|
|
63
|
+
rfNodesRef: import("react").MutableRefObject<import("reactflow").Node[]>;
|
|
64
|
+
rfEdgesRef: import("react").MutableRefObject<import("reactflow").Edge[]>;
|
|
66
65
|
viewIdRef: import("react").MutableRefObject<number | null>;
|
|
67
66
|
refreshGrid: () => Promise<void>;
|
|
68
67
|
refreshElements: () => Promise<void>;
|
|
69
68
|
handleElementDeleted: (deletedId: number) => void;
|
|
70
69
|
handleElementPermanentlyDeleted: (deletedId: number) => void;
|
|
71
70
|
handleElementSaved: (saved: LibraryElement) => void;
|
|
72
|
-
setAllElements:
|
|
71
|
+
setAllElements: (next: import("../../../store/useStore").StoreSetter<LibraryElement[]>) => void;
|
|
73
72
|
};
|
|
74
73
|
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Edge as RFEdge, Node as RFNode } from 'reactflow';
|
|
2
|
+
import type { Connector, IncomingViewConnector, LibraryElement, PlacedElement, ViewConnector, ViewTreeNode } from '../types';
|
|
3
|
+
export type StoreSetter<T> = T | ((previous: T) => T);
|
|
4
|
+
export type ViewEditorUiState = {
|
|
5
|
+
viewId: number | null;
|
|
6
|
+
canEdit: boolean;
|
|
7
|
+
isOwner: boolean;
|
|
8
|
+
isFreePlan: boolean;
|
|
9
|
+
snapToGrid: boolean;
|
|
10
|
+
selectedElement: LibraryElement | null;
|
|
11
|
+
selectedConnector: Connector | null;
|
|
12
|
+
};
|
|
13
|
+
export type ViewContentLinks = {
|
|
14
|
+
linksMap: Record<number, ViewConnector[]>;
|
|
15
|
+
parentLinksMap: Record<number, ViewConnector[]>;
|
|
16
|
+
incomingLinks: IncomingViewConnector[];
|
|
17
|
+
};
|
|
18
|
+
export type ViewContentPayload = ViewContentLinks & {
|
|
19
|
+
view: ViewTreeNode | null;
|
|
20
|
+
viewElements: PlacedElement[];
|
|
21
|
+
connectors: Connector[];
|
|
22
|
+
treeData: ViewTreeNode[];
|
|
23
|
+
};
|
|
24
|
+
export type CanvasStoreState = ViewEditorUiState & {
|
|
25
|
+
view: ViewTreeNode | null | undefined;
|
|
26
|
+
viewElements: PlacedElement[];
|
|
27
|
+
connectors: Connector[];
|
|
28
|
+
nodes: RFNode[];
|
|
29
|
+
edges: RFEdge[];
|
|
30
|
+
linksMap: Record<number, ViewConnector[]>;
|
|
31
|
+
parentLinksMap: Record<number, ViewConnector[]>;
|
|
32
|
+
incomingLinks: IncomingViewConnector[];
|
|
33
|
+
treeData: ViewTreeNode[];
|
|
34
|
+
allElements: LibraryElement[];
|
|
35
|
+
libraryRefresh: number;
|
|
36
|
+
setViewEditorUi: (patch: Partial<ViewEditorUiState>) => void;
|
|
37
|
+
setSnapToGrid: (snapToGrid: boolean) => void;
|
|
38
|
+
setSelectedElement: (selectedElement: LibraryElement | null) => void;
|
|
39
|
+
setSelectedConnector: (selectedConnector: Connector | null) => void;
|
|
40
|
+
setView: (view: ViewTreeNode | null | undefined) => void;
|
|
41
|
+
setViewElements: (next: StoreSetter<PlacedElement[]>) => void;
|
|
42
|
+
setConnectors: (next: StoreSetter<Connector[]>) => void;
|
|
43
|
+
setNodes: (next: StoreSetter<RFNode[]>) => void;
|
|
44
|
+
setEdges: (next: StoreSetter<RFEdge[]>) => void;
|
|
45
|
+
setLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void;
|
|
46
|
+
setParentLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void;
|
|
47
|
+
setIncomingLinks: (next: StoreSetter<IncomingViewConnector[]>) => void;
|
|
48
|
+
setTreeData: (next: StoreSetter<ViewTreeNode[]>) => void;
|
|
49
|
+
setAllElements: (next: StoreSetter<LibraryElement[]>) => void;
|
|
50
|
+
setLibraryRefresh: (next: StoreSetter<number>) => void;
|
|
51
|
+
resetCanvas: () => void;
|
|
52
|
+
hydrateViewContent: (payload: ViewContentPayload) => void;
|
|
53
|
+
updateElementPosition: (elementId: number, x: number, y: number) => void;
|
|
54
|
+
removeElementPlacement: (elementId: number) => void;
|
|
55
|
+
removeElementEverywhere: (elementId: number) => void;
|
|
56
|
+
mergeSavedElement: (saved: LibraryElement) => void;
|
|
57
|
+
upsertConnector: (connector: Connector) => void;
|
|
58
|
+
replaceConnector: (connector: Connector) => void;
|
|
59
|
+
removeConnector: (connectorId: number) => void;
|
|
60
|
+
};
|
|
61
|
+
export declare const emptyViewEditorUiState: ViewEditorUiState;
|
|
62
|
+
export declare function findViewByOwner(nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null;
|
|
63
|
+
export declare function findViewPath(nodes: ViewTreeNode[], targetId: number, path?: ViewTreeNode[]): ViewTreeNode[] | null;
|
|
64
|
+
export declare function buildViewContentLinks(tree: ViewTreeNode[], viewId: number, viewElements: PlacedElement[]): ViewContentLinks;
|
|
65
|
+
export declare function selectExistingElementIds(state: Pick<CanvasStoreState, 'viewElements'>): Set<number>;
|
|
66
|
+
export declare function selectElementById(state: Pick<CanvasStoreState, 'viewElements'>, elementId: number): PlacedElement | undefined;
|
|
67
|
+
export declare function selectConnectorById(state: Pick<CanvasStoreState, 'connectors'>, connectorId: number): Connector | undefined;
|
|
68
|
+
export declare function updatePlacedElementPosition(elements: PlacedElement[], elementId: number, x: number, y: number): PlacedElement[];
|
|
69
|
+
export declare function removePlacedElement(elements: PlacedElement[], elementId: number): PlacedElement[];
|
|
70
|
+
export declare function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[];
|
|
71
|
+
export declare function upsertConnectorInList(connectors: Connector[], connector: Connector): Connector[];
|
|
72
|
+
export declare function removeConnectorFromList(connectors: Connector[], connectorId: number): Connector[];
|
|
73
|
+
export declare const useStore: import("zustand").UseBoundStore<import("zustand").StoreApi<CanvasStoreState>>;
|
|
74
|
+
export declare const canvasSelectors: {
|
|
75
|
+
existingElementIds: typeof selectExistingElementIds;
|
|
76
|
+
elementById: typeof selectElementById;
|
|
77
|
+
connectorById: typeof selectConnectorById;
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldiagram/core-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.93.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@buf/tldiagramcom_diagram.bufbuild_es": "^2.11.0-20260419172603-8192c519478e.1",
|
|
40
40
|
"@bufbuild/protobuf": "^2.11.0",
|
|
41
|
-
"
|
|
41
|
+
"@tanstack/react-query": "^5.100.1",
|
|
42
|
+
"esbuild": "^0.25.12",
|
|
43
|
+
"zustand": "^5.0.12"
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
44
46
|
"@chakra-ui/icons": "^2.2.4",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"@emotion/styled": "^11.14.0",
|
|
60
62
|
"@uiw/react-codemirror": "^4.25.8",
|
|
61
63
|
"d3-force": "^3.0.0",
|
|
62
|
-
"
|
|
64
|
+
"dagre": "^0.8.5",
|
|
63
65
|
"framer-motion": "^11.18.2",
|
|
64
66
|
"html-to-image": "^1.11.13",
|
|
65
67
|
"mermaid-ast": "^0.8.2",
|
|
@@ -125,12 +127,13 @@
|
|
|
125
127
|
"@emotion/react": "^11.14.0",
|
|
126
128
|
"@emotion/styled": "^11.14.0",
|
|
127
129
|
"@eslint/js": "^9.0.0",
|
|
130
|
+
"@types/dagre": "^0.7.54",
|
|
128
131
|
"@types/react": "^18.3.12",
|
|
129
132
|
"@types/react-dom": "^18.3.1",
|
|
130
133
|
"@uiw/react-codemirror": "^4.25.8",
|
|
131
134
|
"@vitejs/plugin-react": "^4.3.4",
|
|
132
135
|
"d3-force": "^3.0.0",
|
|
133
|
-
"
|
|
136
|
+
"dagre": "^0.8.5",
|
|
134
137
|
"eslint": "^9.0.0",
|
|
135
138
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
136
139
|
"eslint-plugin-react-refresh": "^0.4.0",
|
package/src/api/client.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
ImportService,
|
|
47
47
|
} from '@buf/tldiagramcom_diagram.bufbuild_es/diag/v1/import_service_pb'
|
|
48
48
|
import { transport } from './transport'
|
|
49
|
+
import { apiUrl, fetchApiAsset } from '../config/runtime'
|
|
49
50
|
|
|
50
51
|
export interface DependenciesResponse {
|
|
51
52
|
elements: DependencyElement[]
|
|
@@ -383,6 +384,36 @@ export const api = {
|
|
|
383
384
|
return (json.views ?? []).map(mapDiagram)
|
|
384
385
|
}),
|
|
385
386
|
|
|
387
|
+
// Lazy: root-level views only. Use for sidebar first render on huge workspaces.
|
|
388
|
+
treeRoots: (opts: { limit?: number; offset?: number; search?: string } = {}): Promise<{ views: ViewTreeNode[]; totalCount: number }> =>
|
|
389
|
+
rpc(async () => {
|
|
390
|
+
const res = await workspaceClient.getWorkspace({
|
|
391
|
+
includeContent: false,
|
|
392
|
+
level: 0,
|
|
393
|
+
limit: opts.limit ?? 0,
|
|
394
|
+
offset: opts.offset ?? 0,
|
|
395
|
+
search: opts.search ?? '',
|
|
396
|
+
})
|
|
397
|
+
const json = j<{ views: ProtoDiagram[]; total_count?: number }>(GetWorkspaceResponseSchema, res)
|
|
398
|
+
return {
|
|
399
|
+
views: (json.views ?? []).map(mapDiagram),
|
|
400
|
+
totalCount: Number(json.total_count ?? 0),
|
|
401
|
+
}
|
|
402
|
+
}),
|
|
403
|
+
|
|
404
|
+
// Lazy: direct children of a parent view. Used on tree node expand.
|
|
405
|
+
treeChildren: (parentId: number, opts: { limit?: number; offset?: number } = {}): Promise<ViewTreeNode[]> =>
|
|
406
|
+
rpc(async () => {
|
|
407
|
+
const res = await workspaceClient.getWorkspace({
|
|
408
|
+
includeContent: false,
|
|
409
|
+
parentId,
|
|
410
|
+
limit: opts.limit ?? 0,
|
|
411
|
+
offset: opts.offset ?? 0,
|
|
412
|
+
})
|
|
413
|
+
const json = j<{ views: ProtoDiagram[] }>(GetWorkspaceResponseSchema, res)
|
|
414
|
+
return (json.views ?? []).map(mapDiagram)
|
|
415
|
+
}),
|
|
416
|
+
|
|
386
417
|
get: (id: number): Promise<ViewTreeNode> =>
|
|
387
418
|
rpc(async () => {
|
|
388
419
|
const res = await workspaceClient.getView({ viewId: id })
|
|
@@ -422,7 +453,14 @@ export const api = {
|
|
|
422
453
|
delete: (_orgId: string, id: number): Promise<void> =>
|
|
423
454
|
rpc(async () => { await workspaceClient.deleteView({ orgId: '', viewId: id }) }),
|
|
424
455
|
|
|
425
|
-
thumbnail: async (
|
|
456
|
+
thumbnail: async (id: number): Promise<string | null> => {
|
|
457
|
+
const res = await fetchApiAsset(apiUrl(`/views/${id}/thumbnail.svg`), {
|
|
458
|
+
headers: { Accept: 'image/svg+xml' },
|
|
459
|
+
})
|
|
460
|
+
if (!res.ok) return null
|
|
461
|
+
const svg = await res.text()
|
|
462
|
+
return URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }))
|
|
463
|
+
},
|
|
426
464
|
|
|
427
465
|
placements: {
|
|
428
466
|
list: (diagramId: number): Promise<ElementPlacement[]> =>
|
|
@@ -12,9 +12,6 @@ import { ZoomInIcon, ZoomOutIcon, TrashIcon as TrashSvg, EditIcon as EditSvg } f
|
|
|
12
12
|
import { vscodeBridge } from '../lib/vscodeBridge'
|
|
13
13
|
import type { ExtensionToWebviewMessage } from '../types/vscode-messages'
|
|
14
14
|
import {
|
|
15
|
-
DEFAULT_SOURCE_HANDLE_SIDE,
|
|
16
|
-
DEFAULT_TARGET_HANDLE_SIDE,
|
|
17
|
-
ensureVisualHandleId,
|
|
18
15
|
getVisualHandleId,
|
|
19
16
|
getVisualHandleStyle,
|
|
20
17
|
HANDLE_SLOT_CENTER_INDEX,
|
|
@@ -154,6 +151,10 @@ interface NodeData extends PlacedElement {
|
|
|
154
151
|
layerHighlightColor?: string
|
|
155
152
|
forceShowTagPopup?: boolean
|
|
156
153
|
isCanvasMoving?: boolean
|
|
154
|
+
connectedHandleIds?: readonly string[]
|
|
155
|
+
selectedHandleIds?: readonly string[]
|
|
156
|
+
reconnectCandidates?: readonly { handleId: string; edgeId: string; endpoint: 'source' | 'target'; selected: boolean }[]
|
|
157
|
+
isConnectorHighlighted?: boolean
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
interface Props {
|
|
@@ -285,61 +286,13 @@ function ElementNode({ data, selected }: Props) {
|
|
|
285
286
|
const zoom = useStore(zoomSelector)
|
|
286
287
|
useAccentColor()
|
|
287
288
|
|
|
288
|
-
const nodeId = String(data.element_id)
|
|
289
|
-
const isConnectorHighlighted = useStore((s) =>
|
|
290
|
-
s.edges.some(e => e.selected && (e.source === nodeId || e.target === nodeId))
|
|
291
|
-
)
|
|
292
|
-
const { connectedHandleKey, selectedHandleKey } = useStore((s) => {
|
|
293
|
-
const connectedHandles = new Set<string>()
|
|
294
|
-
const selectedHandles = new Set<string>()
|
|
295
|
-
for (const connector of s.edges) {
|
|
296
|
-
if (connector.source === nodeId) {
|
|
297
|
-
const targetNode = s.nodeInternals.get(connector.target)
|
|
298
|
-
if (targetNode && targetNode.type !== 'ContextBoundaryElement' && targetNode.type !== 'contextNeighborNode') {
|
|
299
|
-
const handleId = ensureVisualHandleId(connector.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE)
|
|
300
|
-
if (handleId) {
|
|
301
|
-
connectedHandles.add(handleId)
|
|
302
|
-
if (connector.selected) selectedHandles.add(handleId)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (connector.target === nodeId) {
|
|
307
|
-
const sourceNode = s.nodeInternals.get(connector.source)
|
|
308
|
-
if (sourceNode && sourceNode.type !== 'ContextBoundaryElement' && sourceNode.type !== 'contextNeighborNode') {
|
|
309
|
-
const handleId = ensureVisualHandleId(connector.targetHandle, DEFAULT_TARGET_HANDLE_SIDE)
|
|
310
|
-
if (handleId) {
|
|
311
|
-
connectedHandles.add(handleId)
|
|
312
|
-
if (connector.selected) selectedHandles.add(handleId)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
connectedHandleKey: Array.from(connectedHandles).sort().join('|'),
|
|
319
|
-
selectedHandleKey: Array.from(selectedHandles).sort().join('|'),
|
|
320
|
-
}
|
|
321
|
-
})
|
|
322
|
-
const handleReconnectCandidates = useStore((s) => {
|
|
323
|
-
const candidates: Array<{ handleId: string; edgeId: string; endpoint: 'source' | 'target'; selected: boolean }> = []
|
|
324
|
-
for (const connector of s.edges) {
|
|
325
|
-
if (connector.source === nodeId) {
|
|
326
|
-
const handleId = ensureVisualHandleId(connector.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE)
|
|
327
|
-
if (handleId) candidates.push({ handleId, edgeId: connector.id, endpoint: 'source', selected: !!connector.selected })
|
|
328
|
-
}
|
|
329
|
-
if (connector.target === nodeId) {
|
|
330
|
-
const handleId = ensureVisualHandleId(connector.targetHandle, DEFAULT_TARGET_HANDLE_SIDE)
|
|
331
|
-
if (handleId) candidates.push({ handleId, edgeId: connector.id, endpoint: 'target', selected: !!connector.selected })
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return candidates
|
|
335
|
-
})
|
|
336
289
|
const connectedHandleIds = useMemo(
|
|
337
|
-
() => new Set(
|
|
338
|
-
[
|
|
290
|
+
() => new Set(data.connectedHandleIds ?? []),
|
|
291
|
+
[data.connectedHandleIds],
|
|
339
292
|
)
|
|
340
293
|
const selectedHandleIds = useMemo(
|
|
341
|
-
() => new Set(
|
|
342
|
-
[
|
|
294
|
+
() => new Set(data.selectedHandleIds ?? []),
|
|
295
|
+
[data.selectedHandleIds],
|
|
343
296
|
)
|
|
344
297
|
const activeSides = useMemo(() => {
|
|
345
298
|
const sides = new Set<string>()
|
|
@@ -351,7 +304,7 @@ function ElementNode({ data, selected }: Props) {
|
|
|
351
304
|
}, [connectedHandleIds])
|
|
352
305
|
const reconnectCandidateByHandle = useMemo(() => {
|
|
353
306
|
const next = new Map<string, { edgeId: string; endpoint: 'source' | 'target' }>()
|
|
354
|
-
const sortedCandidates = [...
|
|
307
|
+
const sortedCandidates = [...(data.reconnectCandidates ?? [])].sort((left, right) => {
|
|
355
308
|
if (left.selected !== right.selected) return left.selected ? -1 : 1
|
|
356
309
|
return left.edgeId.localeCompare(right.edgeId)
|
|
357
310
|
})
|
|
@@ -361,7 +314,7 @@ function ElementNode({ data, selected }: Props) {
|
|
|
361
314
|
}
|
|
362
315
|
}
|
|
363
316
|
return next
|
|
364
|
-
}, [
|
|
317
|
+
}, [data.reconnectCandidates])
|
|
365
318
|
|
|
366
319
|
const derivedPrimaryIconPath = (() => {
|
|
367
320
|
const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
|
|
@@ -509,7 +462,7 @@ function ElementNode({ data, selected }: Props) {
|
|
|
509
462
|
isSelected={selected}
|
|
510
463
|
isSource={isSource}
|
|
511
464
|
isTarget={isTarget}
|
|
512
|
-
isConnectorHighlighted={isConnectorHighlighted}
|
|
465
|
+
isConnectorHighlighted={!!data.isConnectorHighlighted}
|
|
513
466
|
minW="180px"
|
|
514
467
|
maxW="230px"
|
|
515
468
|
cursor={bodyCursor}
|
|
@@ -480,7 +480,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
480
480
|
try {
|
|
481
481
|
if (viewId != null) {
|
|
482
482
|
await api.workspace.views.placements.remove(viewId, element.id)
|
|
483
|
-
} else if (orgId) {
|
|
483
|
+
} else if (orgId !== undefined) {
|
|
484
484
|
await api.elements.delete(orgId, element.id)
|
|
485
485
|
}
|
|
486
486
|
onDelete?.(element.id)
|
|
@@ -491,7 +491,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
491
491
|
const handlePermanentDelete = async () => {
|
|
492
492
|
if (isReadOnly || !element) return
|
|
493
493
|
try {
|
|
494
|
-
if (
|
|
494
|
+
if (orgId === undefined) return
|
|
495
495
|
await api.elements.delete(orgId, element.id)
|
|
496
496
|
onPermanentDelete?.(element.id)
|
|
497
497
|
confirmPermanentDelete.onClose()
|
|
@@ -21,11 +21,10 @@ import { ChevronDownIcon, ChevronRightIcon } from './Icons'
|
|
|
21
21
|
import { api } from '../api/client'
|
|
22
22
|
import type { ViewTreeNode } from '../types'
|
|
23
23
|
|
|
24
|
-
type Algorithm = '
|
|
24
|
+
type Algorithm = 'dagre' | 'force'
|
|
25
25
|
|
|
26
|
-
interface
|
|
27
|
-
|
|
28
|
-
direction: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP'
|
|
26
|
+
interface DagreConfig {
|
|
27
|
+
direction: 'TB' | 'BT' | 'LR' | 'RL'
|
|
29
28
|
nodeSpacing: number
|
|
30
29
|
layerSpacing: number
|
|
31
30
|
}
|
|
@@ -41,7 +40,7 @@ const NODE_W = 200
|
|
|
41
40
|
const NODE_H = 120
|
|
42
41
|
|
|
43
42
|
const ALGO_META: Record<Algorithm, { label: string }> = {
|
|
44
|
-
|
|
43
|
+
dagre: { label: 'Layered' },
|
|
45
44
|
force: { label: 'Organic' },
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -52,13 +51,12 @@ interface Props {
|
|
|
52
51
|
|
|
53
52
|
export default function LayoutSection({ view, canEdit }: Props) {
|
|
54
53
|
const [open, setOpen] = useState(false)
|
|
55
|
-
const [algo, setAlgo] = useState<Algorithm>('
|
|
54
|
+
const [algo, setAlgo] = useState<Algorithm>('dagre')
|
|
56
55
|
const [running, setRunning] = useState(false)
|
|
57
56
|
const [collisionRunning, setCollisionRunning] = useState(false)
|
|
58
57
|
|
|
59
|
-
const [
|
|
60
|
-
|
|
61
|
-
direction: 'DOWN',
|
|
58
|
+
const [dagreConfig, setDagreConfig] = useState<DagreConfig>({
|
|
59
|
+
direction: 'TB',
|
|
62
60
|
nodeSpacing: 75,
|
|
63
61
|
layerSpacing: 75,
|
|
64
62
|
})
|
|
@@ -199,8 +197,8 @@ export default function LayoutSection({ view, canEdit }: Props) {
|
|
|
199
197
|
])
|
|
200
198
|
|
|
201
199
|
let positions: Map<number, { x: number; y: number }>
|
|
202
|
-
if (algo === '
|
|
203
|
-
positions = await
|
|
200
|
+
if (algo === 'dagre') {
|
|
201
|
+
positions = await runDagre(objs, edgeList)
|
|
204
202
|
} else {
|
|
205
203
|
positions = await runForce(objs, edgeList)
|
|
206
204
|
}
|
|
@@ -271,46 +269,45 @@ export default function LayoutSection({ view, canEdit }: Props) {
|
|
|
271
269
|
}
|
|
272
270
|
|
|
273
271
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
274
|
-
const
|
|
272
|
+
const runDagre = async (objs: any[], edgeList: any[]) => {
|
|
275
273
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const layoutOptions: Record<string, string> = {
|
|
280
|
-
'elk.algorithm': elkConfig.algorithm,
|
|
281
|
-
'elk.spacing.nodeNode': String(elkConfig.nodeSpacing),
|
|
282
|
-
}
|
|
283
|
-
if (elkConfig.algorithm === 'layered') {
|
|
284
|
-
layoutOptions['elk.direction'] = elkConfig.direction
|
|
285
|
-
layoutOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(elkConfig.layerSpacing)
|
|
286
|
-
}
|
|
274
|
+
const dagreModule = await import('dagre') as any
|
|
275
|
+
const dagre = dagreModule.default ?? dagreModule
|
|
287
276
|
|
|
288
277
|
const objSet = new Set<number>(objs.map((o: { element_id?: number }) => Number(o.element_id)))
|
|
289
|
-
const graph = {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
278
|
+
const graph = new dagre.graphlib.Graph({ multigraph: true })
|
|
279
|
+
graph.setGraph({
|
|
280
|
+
rankdir: dagreConfig.direction,
|
|
281
|
+
nodesep: dagreConfig.nodeSpacing,
|
|
282
|
+
ranksep: dagreConfig.layerSpacing,
|
|
283
|
+
marginx: 0,
|
|
284
|
+
marginy: 0,
|
|
285
|
+
})
|
|
286
|
+
graph.setDefaultEdgeLabel(() => ({}))
|
|
287
|
+
|
|
288
|
+
objs.forEach((obj: { element_id: number }) => {
|
|
289
|
+
graph.setNode(String(obj.element_id), { width: NODE_W, height: NODE_H })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
edgeList
|
|
293
|
+
.filter((e: { source_element_id: number; target_element_id: number }) =>
|
|
294
|
+
objSet.has(e.source_element_id) && objSet.has(e.target_element_id)
|
|
295
|
+
)
|
|
296
|
+
.forEach((e: { id: number; source_element_id: number; target_element_id: number }) => {
|
|
297
|
+
graph.setEdge(String(e.source_element_id), String(e.target_element_id), {}, String(e.id))
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
dagre.layout(graph)
|
|
307
301
|
|
|
308
|
-
const result = await elk.layout(graph)
|
|
309
302
|
const positions = new Map<number, { x: number; y: number }>()
|
|
310
|
-
|
|
311
|
-
const id = Number(
|
|
303
|
+
graph.nodes().forEach((nodeId: string) => {
|
|
304
|
+
const id = Number(nodeId)
|
|
312
305
|
if (!Number.isFinite(id)) return
|
|
313
|
-
|
|
306
|
+
const node = graph.node(nodeId) as { x?: number; y?: number }
|
|
307
|
+
positions.set(id, {
|
|
308
|
+
x: (node.x ?? 0) - NODE_W / 2,
|
|
309
|
+
y: (node.y ?? 0) - NODE_H / 2,
|
|
310
|
+
})
|
|
314
311
|
})
|
|
315
312
|
return positions
|
|
316
313
|
}
|
|
@@ -428,54 +425,34 @@ export default function LayoutSection({ view, canEdit }: Props) {
|
|
|
428
425
|
border="1px solid"
|
|
429
426
|
borderColor="whiteAlpha.100"
|
|
430
427
|
>
|
|
431
|
-
{algo === '
|
|
428
|
+
{algo === 'dagre' ? (
|
|
432
429
|
<Grid templateColumns="1fr 1fr" gap={4}>
|
|
433
430
|
<FormControl gridColumn="span 2">
|
|
434
|
-
<FormLabel {...LabelStyle}>
|
|
431
|
+
<FormLabel {...LabelStyle}>Direction</FormLabel>
|
|
435
432
|
<Select
|
|
436
433
|
size="xs"
|
|
437
434
|
variant="filled"
|
|
438
435
|
bg="whiteAlpha.100"
|
|
439
436
|
border="none"
|
|
440
437
|
_hover={{ bg: 'whiteAlpha.200' }}
|
|
441
|
-
value={
|
|
442
|
-
onChange={e =>
|
|
438
|
+
value={dagreConfig.direction}
|
|
439
|
+
onChange={e => setDagreConfig(c => ({ ...c, direction: e.target.value as DagreConfig['direction'] }))}
|
|
443
440
|
>
|
|
444
|
-
<option value="
|
|
445
|
-
<option value="
|
|
446
|
-
<option value="
|
|
447
|
-
<option value="
|
|
441
|
+
<option value="TB">Top → Bottom</option>
|
|
442
|
+
<option value="BT">Bottom → Top</option>
|
|
443
|
+
<option value="LR">Left → Right</option>
|
|
444
|
+
<option value="RL">Right → Left</option>
|
|
448
445
|
</Select>
|
|
449
446
|
</FormControl>
|
|
450
447
|
|
|
451
|
-
{elkConfig.algorithm === 'layered' && (
|
|
452
|
-
<FormControl gridColumn="span 2">
|
|
453
|
-
<FormLabel {...LabelStyle}>Direction</FormLabel>
|
|
454
|
-
<Select
|
|
455
|
-
size="xs"
|
|
456
|
-
variant="filled"
|
|
457
|
-
bg="whiteAlpha.100"
|
|
458
|
-
border="none"
|
|
459
|
-
_hover={{ bg: 'whiteAlpha.200' }}
|
|
460
|
-
value={elkConfig.direction}
|
|
461
|
-
onChange={e => setElkConfig(c => ({ ...c, direction: e.target.value as ElkConfig['direction'] }))}
|
|
462
|
-
>
|
|
463
|
-
<option value="DOWN">Top → Bottom</option>
|
|
464
|
-
<option value="UP">Bottom → Top</option>
|
|
465
|
-
<option value="RIGHT">Left → Right</option>
|
|
466
|
-
<option value="LEFT">Right → Left</option>
|
|
467
|
-
</Select>
|
|
468
|
-
</FormControl>
|
|
469
|
-
)}
|
|
470
|
-
|
|
471
448
|
<FormControl>
|
|
472
449
|
<FormLabel {...LabelStyle}>Element Gap</FormLabel>
|
|
473
450
|
<NumberInput
|
|
474
451
|
size="xs"
|
|
475
452
|
variant="filled"
|
|
476
|
-
value={
|
|
453
|
+
value={dagreConfig.nodeSpacing}
|
|
477
454
|
min={10} max={400} step={10}
|
|
478
|
-
onChange={(_, v) => !isNaN(v) &&
|
|
455
|
+
onChange={(_, v) => !isNaN(v) && setDagreConfig(c => ({ ...c, nodeSpacing: v }))}
|
|
479
456
|
>
|
|
480
457
|
<NumberInputField bg="whiteAlpha.100" border="none" />
|
|
481
458
|
<NumberInputStepper>
|
|
@@ -485,24 +462,22 @@ export default function LayoutSection({ view, canEdit }: Props) {
|
|
|
485
462
|
</NumberInput>
|
|
486
463
|
</FormControl>
|
|
487
464
|
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
</FormControl>
|
|
505
|
-
)}
|
|
465
|
+
<FormControl>
|
|
466
|
+
<FormLabel {...LabelStyle}>Layer Gap</FormLabel>
|
|
467
|
+
<NumberInput
|
|
468
|
+
size="xs"
|
|
469
|
+
variant="filled"
|
|
470
|
+
value={dagreConfig.layerSpacing}
|
|
471
|
+
min={10} max={400} step={10}
|
|
472
|
+
onChange={(_, v) => !isNaN(v) && setDagreConfig(c => ({ ...c, layerSpacing: v }))}
|
|
473
|
+
>
|
|
474
|
+
<NumberInputField bg="whiteAlpha.100" border="none" />
|
|
475
|
+
<NumberInputStepper>
|
|
476
|
+
<NumberIncrementStepper border="none" />
|
|
477
|
+
<NumberDecrementStepper border="none" />
|
|
478
|
+
</NumberInputStepper>
|
|
479
|
+
</NumberInput>
|
|
480
|
+
</FormControl>
|
|
506
481
|
</Grid>
|
|
507
482
|
) : (
|
|
508
483
|
<Grid templateColumns="1fr 1fr" gap={4}>
|