@likec4/diagram 1.6.1 → 1.7.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/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +10 -11
- package/dist/hooks/useUpdateEffect.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +572 -464
- package/dist/state/DiagramContext.d.ts +746 -181
- package/dist/state/diagram-to-xyflow.d.ts +10 -0
- package/dist/state/diagramStore.d.ts +782 -202
- package/dist/style.css +273 -275
- package/dist/types.d.ts +20 -0
- package/dist/xyflow/EdgeMarkers.d.ts +1 -0
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +1 -4
- package/dist/xyflow/XYFlowEvents.d.ts +41 -12
- package/dist/xyflow/XYFlowInner.d.ts +0 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
- package/dist/xyflow/hooks/index.d.ts +0 -1
- package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
- package/dist/xyflow/types.d.ts +11 -19
- package/dist/xyflow/utils.d.ts +1 -0
- package/package.json +6 -6
- package/src/LikeC4Diagram.props.ts +17 -13
- package/src/LikeC4Diagram.tsx +62 -96
- package/src/globals.css.ts +10 -6
- package/src/hooks/useUpdateEffect.ts +16 -1
- package/src/index.ts +1 -0
- package/src/state/DiagramContext.tsx +19 -27
- package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
- package/src/state/diagramStore.ts +404 -192
- package/src/types.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +24 -7
- package/src/xyflow/EdgeMarkers.tsx +2 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
- package/src/xyflow/XYFlow.tsx +57 -32
- package/src/xyflow/XYFlowEvents.tsx +58 -10
- package/src/xyflow/XYFlowInner.tsx +8 -8
- package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
- package/src/xyflow/edges/edges.css.ts +3 -3
- package/src/xyflow/edges/utils.ts +0 -1
- package/src/xyflow/hooks/index.ts +0 -1
- package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
- package/src/xyflow/nodes/element/ElementNode.tsx +0 -4
- package/src/xyflow/nodes/element/element.css.ts +8 -6
- package/src/xyflow/types.ts +14 -32
- package/src/xyflow/utils.ts +4 -0
- package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
- package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
- package/src/xyflow/SyncWithDiagram.tsx +0 -85
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { DiagramNode, DiagramView, Fqn, ViewID } from '@likec4/core'
|
|
2
|
-
import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import type { DiagramNode, DiagramView, Fqn, NodeId, ViewID } from '@likec4/core'
|
|
2
|
+
import { getBBoxCenter, invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
|
|
3
|
+
import {
|
|
4
|
+
applyEdgeChanges,
|
|
5
|
+
applyNodeChanges,
|
|
6
|
+
getViewportForBounds,
|
|
7
|
+
type OnEdgesChange,
|
|
8
|
+
type OnNodesChange
|
|
9
|
+
} from '@xyflow/react'
|
|
10
|
+
import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
|
|
5
11
|
import { DEV } from 'esm-env'
|
|
6
|
-
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
12
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
7
13
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
8
|
-
import { entries,
|
|
9
|
-
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
14
|
+
import { entries, hasAtLeast, isNullish, reduce } from 'remeda'
|
|
15
|
+
import type { ConditionalKeys, Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
10
16
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
11
17
|
import { shallow } from 'zustand/shallow'
|
|
12
18
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
@@ -19,11 +25,14 @@ import type {
|
|
|
19
25
|
import { MinZoom } from '../xyflow/const'
|
|
20
26
|
import type { XYStoreApi } from '../xyflow/hooks'
|
|
21
27
|
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
22
|
-
import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
|
|
28
|
+
import { bezierControlPoints, isSamePoint, toDomPrecision } from '../xyflow/utils'
|
|
29
|
+
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
type RequiredOrNull<T> = {
|
|
32
|
+
[P in keyof T]-?: NonNullable<T[P]> | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DiagramInitialState = {
|
|
27
36
|
view: DiagramView
|
|
28
37
|
readonly: boolean
|
|
29
38
|
showElementLinks: boolean
|
|
@@ -34,104 +43,106 @@ export type DiagramStore = {
|
|
|
34
43
|
nodesDraggable: boolean
|
|
35
44
|
nodesSelectable: boolean
|
|
36
45
|
experimentalEdgeEditing: boolean
|
|
46
|
+
enableFocusMode: boolean
|
|
37
47
|
renderIcon: ElementIconRenderer | null
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Internal state
|
|
42
|
-
viewSyncDebounceTimeout: number | null
|
|
48
|
+
// If Dynamic View
|
|
49
|
+
enableDynamicViewWalkthrough: boolean
|
|
43
50
|
|
|
44
51
|
xyflow: XYFlowInstance
|
|
45
52
|
xystore: XYStoreApi
|
|
46
|
-
initialized: boolean
|
|
47
|
-
xyflowSynced: boolean
|
|
48
|
-
|
|
49
|
-
// If Dynamic View
|
|
50
|
-
activeDynamicViewStep: number | null
|
|
51
|
-
|
|
52
|
-
// This is XYFlow id's
|
|
53
|
-
lastClickedNodeId: string | null
|
|
54
|
-
lastClickedEdgeId: string | null
|
|
55
|
-
focusedNodeId: string | null
|
|
56
|
-
hoveredNodeId: string | null
|
|
57
|
-
hoveredEdgeId: string | null
|
|
58
|
-
|
|
59
|
-
// id's of nodes / edges that
|
|
60
|
-
dimmed: ReadonlySet<string>
|
|
61
|
-
|
|
62
|
-
// Stack of previous distinct views (unique id's)
|
|
63
|
-
previousViews: DiagramView[]
|
|
64
|
-
|
|
65
|
-
lastOnNavigate: null | {
|
|
66
|
-
fromView: ViewID
|
|
67
|
-
toView: ViewID
|
|
68
|
-
element: DiagramNode
|
|
69
|
-
// Position of the element in the source view
|
|
70
|
-
elementScreenPosition: XYPosition
|
|
71
|
-
// If node from the target view is placed on the same position as the node from the source view
|
|
72
|
-
positionCorrected: boolean
|
|
73
|
-
}
|
|
74
53
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
} &
|
|
54
|
+
// Diagram Container, for Mantine Portal
|
|
55
|
+
getContainer: () => HTMLDivElement | null
|
|
56
|
+
} & RequiredOrNull<LikeC4DiagramEventHandlers>
|
|
78
57
|
|
|
79
58
|
const StringSet = Set<string>
|
|
80
59
|
|
|
81
|
-
export type
|
|
82
|
-
&
|
|
83
|
-
|
|
84
|
-
| 'view'
|
|
85
|
-
| 'renderIcon'
|
|
86
|
-
| 'readonly'
|
|
87
|
-
| 'showElementLinks'
|
|
88
|
-
| 'fitViewEnabled'
|
|
89
|
-
| 'fitViewPadding'
|
|
90
|
-
| 'zoomable'
|
|
91
|
-
| 'pannable'
|
|
92
|
-
| 'nodesDraggable'
|
|
93
|
-
| 'nodesSelectable'
|
|
94
|
-
| 'experimentalEdgeEditing'
|
|
95
|
-
>
|
|
96
|
-
& LikeC4DiagramEventHandlers
|
|
60
|
+
export type DiagramState = Simplify<
|
|
61
|
+
DiagramInitialState & {
|
|
62
|
+
readonly storeDevId: string
|
|
97
63
|
|
|
98
|
-
|
|
99
|
-
|
|
64
|
+
// Internal state
|
|
65
|
+
xynodes: XYFlowNode[]
|
|
66
|
+
xyedges: XYFlowEdge[]
|
|
67
|
+
viewSyncDebounceTimeout: number | null
|
|
68
|
+
viewportChanged: boolean
|
|
100
69
|
|
|
101
|
-
|
|
70
|
+
initialized: boolean
|
|
102
71
|
|
|
103
|
-
|
|
72
|
+
// If Dynamic View
|
|
73
|
+
isDynamicView: boolean
|
|
74
|
+
activeDynamicViewStep: number | null
|
|
104
75
|
|
|
105
|
-
|
|
106
|
-
|
|
76
|
+
// This is XYFlow id's
|
|
77
|
+
lastClickedNodeId: string | null
|
|
78
|
+
lastClickedEdgeId: string | null
|
|
79
|
+
focusedNodeId: string | null
|
|
80
|
+
hoveredNodeId: string | null
|
|
81
|
+
hoveredEdgeId: string | null
|
|
107
82
|
|
|
108
|
-
|
|
109
|
-
|
|
83
|
+
// id's of nodes / edges that
|
|
84
|
+
dimmed: ReadonlySet<string>
|
|
110
85
|
|
|
111
|
-
|
|
86
|
+
lastOnNavigate: null | {
|
|
87
|
+
fromView: ViewID
|
|
88
|
+
toView: ViewID
|
|
89
|
+
fromNode: NodeId
|
|
90
|
+
}
|
|
91
|
+
navigationHistory: Array<{
|
|
92
|
+
viewId: ViewID
|
|
93
|
+
nodeId: NodeId | null
|
|
94
|
+
}>
|
|
95
|
+
navigationHistoryIndex: number
|
|
112
96
|
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
// Actions
|
|
98
|
+
updateView: (view: DiagramView) => void
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
triggerSaveManualLayout: () => void
|
|
118
|
-
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
119
|
-
fitDiagram: (duration?: number) => void
|
|
100
|
+
focusOnNode: (nodeId: string) => void
|
|
120
101
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
setHoveredNode: (nodeId: string | null) => void
|
|
103
|
+
setHoveredEdge: (edgeId: string | null) => void
|
|
104
|
+
|
|
105
|
+
setLastClickedNode: (nodeId: string | null) => void
|
|
106
|
+
setLastClickedEdge: (edgeId: string | null) => void
|
|
107
|
+
|
|
108
|
+
resetLastClicked: () => void
|
|
109
|
+
|
|
110
|
+
getElement(id: Fqn): DiagramNode | null
|
|
111
|
+
triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @returns true if there was pending save layout
|
|
115
|
+
*/
|
|
116
|
+
cancelSaveManualLayout: () => boolean
|
|
117
|
+
scheduleSaveManualLayout: () => void
|
|
118
|
+
triggerSaveManualLayout: () => void
|
|
119
|
+
|
|
120
|
+
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
121
|
+
fitDiagram: (duration?: number) => void
|
|
124
122
|
|
|
125
|
-
|
|
123
|
+
goBack: () => void
|
|
124
|
+
goForward: () => void
|
|
125
|
+
|
|
126
|
+
nextDynamicStep: (increment?: number) => void
|
|
127
|
+
activateDynamicStep: (step: number) => void
|
|
128
|
+
stopDynamicView: () => void
|
|
129
|
+
|
|
130
|
+
onInit: (xyflow: XYFlowInstance) => void
|
|
131
|
+
onNodesChange: OnNodesChange<XYFlowNode>
|
|
132
|
+
onEdgesChange: OnEdgesChange<XYFlowEdge>
|
|
133
|
+
}
|
|
134
|
+
>
|
|
126
135
|
|
|
127
136
|
const DEFAULT_PROPS: Except<
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
DiagramState,
|
|
138
|
+
'storeDevId' | 'navigationHistory' | RequiredKeysOf<DiagramInitialState> | ConditionalKeys<DiagramState, Function>
|
|
130
139
|
> = {
|
|
140
|
+
xyedges: [],
|
|
141
|
+
xynodes: [],
|
|
142
|
+
isDynamicView: false,
|
|
131
143
|
viewSyncDebounceTimeout: null,
|
|
132
144
|
initialized: false,
|
|
133
|
-
|
|
134
|
-
previousViews: [],
|
|
145
|
+
navigationHistoryIndex: 0,
|
|
135
146
|
viewportChanged: false,
|
|
136
147
|
activeDynamicViewStep: null,
|
|
137
148
|
focusedNodeId: null,
|
|
@@ -140,28 +151,24 @@ const DEFAULT_PROPS: Except<
|
|
|
140
151
|
lastClickedNodeId: null,
|
|
141
152
|
lastClickedEdgeId: null,
|
|
142
153
|
dimmed: new StringSet(),
|
|
143
|
-
lastOnNavigate: null
|
|
144
|
-
onChange: null,
|
|
145
|
-
onNavigateTo: null,
|
|
146
|
-
onNodeClick: null,
|
|
147
|
-
onNodeContextMenu: null,
|
|
148
|
-
onCanvasContextMenu: null,
|
|
149
|
-
onEdgeClick: null,
|
|
150
|
-
onEdgeContextMenu: null,
|
|
151
|
-
onCanvasClick: null,
|
|
152
|
-
onCanvasDblClick: null
|
|
154
|
+
lastOnNavigate: null
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
export type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xystore' | 'xyflow' | 'getContainer'>
|
|
156
|
-
|
|
157
157
|
const noReplace = false
|
|
158
158
|
|
|
159
159
|
let StoreDevId = 1
|
|
160
160
|
|
|
161
161
|
const EmptyStringSet: ReadonlySet<string> = new StringSet()
|
|
162
162
|
|
|
163
|
-
export function createDiagramStore<T extends Exact<
|
|
163
|
+
export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(props: T) {
|
|
164
164
|
const storeDevId = 'DiagramStore' + String(StoreDevId++).padStart(2, '0')
|
|
165
|
+
const {
|
|
166
|
+
xynodes,
|
|
167
|
+
xyedges
|
|
168
|
+
} = diagramViewToXYFlowData(props.view, {
|
|
169
|
+
draggable: props.nodesDraggable,
|
|
170
|
+
selectable: props.nodesSelectable
|
|
171
|
+
})
|
|
165
172
|
return createWithEqualityFn<
|
|
166
173
|
DiagramState,
|
|
167
174
|
[
|
|
@@ -172,28 +179,49 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
172
179
|
subscribeWithSelector(
|
|
173
180
|
devtools(
|
|
174
181
|
(set, get) => ({
|
|
175
|
-
storeDevId,
|
|
176
182
|
...DEFAULT_PROPS,
|
|
177
|
-
...(props as
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
...(props as DiagramInitialState),
|
|
184
|
+
storeDevId,
|
|
185
|
+
isDynamicView: props.view.__ === 'dynamic',
|
|
186
|
+
xynodes,
|
|
187
|
+
xyedges,
|
|
188
|
+
navigationHistory: [{
|
|
189
|
+
viewId: props.view.id,
|
|
190
|
+
nodeId: null as Fqn | null
|
|
191
|
+
}],
|
|
192
|
+
navigationHistoryIndex: 0,
|
|
193
|
+
|
|
194
|
+
onNodesChange: (changes) => {
|
|
195
|
+
set({
|
|
196
|
+
xynodes: applyNodeChanges(changes, get().xynodes)
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
onEdgesChange: (changes) => {
|
|
200
|
+
set({
|
|
201
|
+
xyedges: applyEdgeChanges(changes, get().xyedges)
|
|
202
|
+
})
|
|
180
203
|
},
|
|
181
204
|
|
|
182
205
|
updateView: (nextView) => {
|
|
183
206
|
let {
|
|
184
207
|
viewSyncDebounceTimeout,
|
|
185
208
|
xyflow,
|
|
209
|
+
xystore,
|
|
186
210
|
dimmed,
|
|
187
|
-
xyflowSynced,
|
|
188
211
|
view: current,
|
|
189
212
|
lastOnNavigate,
|
|
190
|
-
|
|
213
|
+
navigationHistory,
|
|
214
|
+
navigationHistoryIndex,
|
|
191
215
|
focusedNodeId,
|
|
192
216
|
lastClickedNodeId,
|
|
193
217
|
lastClickedEdgeId,
|
|
194
218
|
activeDynamicViewStep,
|
|
219
|
+
nodesDraggable,
|
|
220
|
+
nodesSelectable,
|
|
195
221
|
hoveredEdgeId,
|
|
196
|
-
hoveredNodeId
|
|
222
|
+
hoveredNodeId,
|
|
223
|
+
xyedges,
|
|
224
|
+
xynodes
|
|
197
225
|
} = get()
|
|
198
226
|
|
|
199
227
|
if (shallowEqual(current, nextView)) {
|
|
@@ -230,7 +258,6 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
230
258
|
if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
|
|
231
259
|
activeDynamicViewStep = null
|
|
232
260
|
}
|
|
233
|
-
xyflowSynced = deepEqual([current.nodes, current.edges], [nextView.nodes, nextView.edges])
|
|
234
261
|
if (dimmed.size > 0) {
|
|
235
262
|
let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
|
|
236
263
|
if (nextDimmed.size !== dimmed.size) {
|
|
@@ -239,15 +266,56 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
239
266
|
}
|
|
240
267
|
} else {
|
|
241
268
|
// Reset lastOnNavigate if the view is not the source or target view
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
269
|
+
const stepCurrent = nonNullable(navigationHistory[navigationHistoryIndex])
|
|
270
|
+
if (stepCurrent.viewId !== nextView.id) {
|
|
271
|
+
navigationHistory = [
|
|
272
|
+
...navigationHistory.slice(0, navigationHistoryIndex + 1),
|
|
273
|
+
{
|
|
274
|
+
viewId: nextView.id,
|
|
275
|
+
nodeId: lastOnNavigate?.fromNode || null
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
navigationHistoryIndex = navigationHistory.length - 1
|
|
279
|
+
} else {
|
|
280
|
+
// We are navigating to the same view as in the history
|
|
281
|
+
if (stepCurrent.nodeId) {
|
|
282
|
+
lastOnNavigate ??= {
|
|
283
|
+
fromView: current.id,
|
|
284
|
+
toView: nextView.id,
|
|
285
|
+
fromNode: stepCurrent.nodeId
|
|
286
|
+
}
|
|
245
287
|
}
|
|
246
288
|
}
|
|
247
289
|
|
|
248
|
-
if (
|
|
290
|
+
if (lastOnNavigate && lastOnNavigate.toView !== nextView.id) {
|
|
291
|
+
lastOnNavigate = null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const elTo = lastOnNavigate && nextView.nodes.find(n => n.id === lastOnNavigate?.fromNode)
|
|
295
|
+
const xynodeFrom = elTo && xyflow.getInternalNode(elTo.id)
|
|
296
|
+
|
|
297
|
+
if (!lastOnNavigate || isNullish(elTo) || isNullish(xynodeFrom)) {
|
|
249
298
|
const zoom = xyflow.getZoom()
|
|
250
|
-
|
|
299
|
+
const { x, y } = getBBoxCenter(nextView.bounds)
|
|
300
|
+
xyflow.setCenter(x, y, { zoom })
|
|
301
|
+
lastOnNavigate = null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (lastOnNavigate && !!elTo && !!xynodeFrom) {
|
|
305
|
+
const fromPos = xyflow.flowToScreenPosition({
|
|
306
|
+
x: xynodeFrom.internals.positionAbsolute.x, // + dimensions.width / 2,
|
|
307
|
+
y: xynodeFrom.internals.positionAbsolute.y // + dimensions.height / 2
|
|
308
|
+
}),
|
|
309
|
+
toPos = xyflow.flowToScreenPosition({
|
|
310
|
+
x: elTo.position[0], // + elFrom.width / 2,
|
|
311
|
+
y: elTo.position[1] // + elFrom.height / 2
|
|
312
|
+
}),
|
|
313
|
+
diff = {
|
|
314
|
+
x: toDomPrecision(fromPos.x - toPos.x),
|
|
315
|
+
y: toDomPrecision(fromPos.y - toPos.y)
|
|
316
|
+
}
|
|
317
|
+
xystore.getState().panBy(diff)
|
|
318
|
+
lastOnNavigate = null
|
|
251
319
|
}
|
|
252
320
|
|
|
253
321
|
// Reset hovered / clicked node/edge if the view is different
|
|
@@ -258,29 +326,64 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
258
326
|
focusedNodeId = null
|
|
259
327
|
activeDynamicViewStep = null
|
|
260
328
|
dimmed = EmptyStringSet
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const update = diagramViewToXYFlowData(nextView, {
|
|
332
|
+
draggable: nodesDraggable,
|
|
333
|
+
selectable: nodesSelectable
|
|
334
|
+
})
|
|
261
335
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
336
|
+
update.xynodes = update.xynodes.map(update => {
|
|
337
|
+
const existing = xynodes.find(n => n.id === update.id)
|
|
338
|
+
if (existing && existing.type === update.type && eq(existing.parentId ?? null, update.parentId ?? null)) {
|
|
339
|
+
if (eq(existing.data.element, update.data.element)) {
|
|
340
|
+
return existing
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
...existing,
|
|
344
|
+
...update
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return update
|
|
348
|
+
})
|
|
349
|
+
// Merge with existing edges, but only if the view is the same
|
|
350
|
+
// and the edges have no layout drift
|
|
351
|
+
if (isSameView && !nextView.hasLayoutDrift) {
|
|
352
|
+
update.xyedges = update.xyedges.map((update): XYFlowEdge => {
|
|
353
|
+
const existing = xyedges.find(n => n.id === update.id)
|
|
354
|
+
if (existing) {
|
|
355
|
+
if (eq(existing.data, update.data)) {
|
|
356
|
+
return existing
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
...existing,
|
|
360
|
+
...update
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return update
|
|
364
|
+
})
|
|
268
365
|
}
|
|
269
366
|
|
|
367
|
+
const hasChanges = !isSameView || !shallowEqual(update.xynodes, xynodes)
|
|
368
|
+
|| !shallowEqual(update.xyedges, xyedges)
|
|
369
|
+
|
|
270
370
|
set(
|
|
271
371
|
{
|
|
372
|
+
isDynamicView: nextView.__ === 'dynamic',
|
|
272
373
|
viewSyncDebounceTimeout,
|
|
273
374
|
view: nextView,
|
|
274
375
|
activeDynamicViewStep,
|
|
275
|
-
xyflowSynced,
|
|
276
376
|
lastOnNavigate,
|
|
277
|
-
previousViews,
|
|
278
377
|
lastClickedNodeId,
|
|
279
378
|
lastClickedEdgeId,
|
|
280
379
|
focusedNodeId,
|
|
281
380
|
hoveredEdgeId,
|
|
282
381
|
hoveredNodeId,
|
|
283
|
-
|
|
382
|
+
navigationHistory,
|
|
383
|
+
navigationHistoryIndex,
|
|
384
|
+
dimmed,
|
|
385
|
+
xynodes: hasChanges ? update.xynodes : xynodes,
|
|
386
|
+
xyedges: hasChanges ? update.xyedges : xyedges
|
|
284
387
|
},
|
|
285
388
|
noReplace,
|
|
286
389
|
isSameView ? 'update-view [same]' : 'update-view [another]'
|
|
@@ -288,7 +391,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
288
391
|
},
|
|
289
392
|
|
|
290
393
|
focusOnNode: (nodeId) => {
|
|
291
|
-
const { focusedNodeId, view } = get()
|
|
394
|
+
const { focusedNodeId, view, enableFocusMode } = get()
|
|
395
|
+
invariant(enableFocusMode, 'focus mode is not enabled')
|
|
292
396
|
if (nodeId !== focusedNodeId) {
|
|
293
397
|
const notDimmed = new StringSet([nodeId])
|
|
294
398
|
const dimmed = new StringSet()
|
|
@@ -448,70 +552,113 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
448
552
|
if (viewSyncDebounceTimeout !== null) {
|
|
449
553
|
clearTimeout(viewSyncDebounceTimeout)
|
|
450
554
|
set({ viewSyncDebounceTimeout: null })
|
|
555
|
+
return true
|
|
451
556
|
}
|
|
557
|
+
return false
|
|
452
558
|
},
|
|
453
559
|
|
|
454
560
|
triggerSaveManualLayout: () => {
|
|
455
|
-
|
|
561
|
+
const { xyflow, view, onChange, xystore, viewSyncDebounceTimeout } = get()
|
|
562
|
+
if (viewSyncDebounceTimeout !== null) {
|
|
563
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
564
|
+
set({ viewSyncDebounceTimeout: null })
|
|
565
|
+
}
|
|
456
566
|
if (!onChange) {
|
|
457
567
|
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
458
568
|
return
|
|
459
569
|
}
|
|
460
|
-
|
|
461
|
-
|
|
570
|
+
const { nodeLookup } = xystore.getState()
|
|
571
|
+
const movedNodes = new StringSet()
|
|
572
|
+
let bounds = {
|
|
573
|
+
x: 0,
|
|
574
|
+
y: 0,
|
|
575
|
+
width: 1,
|
|
576
|
+
height: 1
|
|
462
577
|
}
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
controlPoints = bezierControlPoints(data.edge)
|
|
487
|
-
}
|
|
488
|
-
if (controlPoints) {
|
|
489
|
-
acc[data.edge.id] = {
|
|
490
|
-
controlPoints
|
|
491
|
-
}
|
|
492
|
-
}
|
|
578
|
+
const nodes = reduce([...nodeLookup.values()], (acc, node) => {
|
|
579
|
+
const dimensions = getNodeDimensions(node)
|
|
580
|
+
if (!isSamePoint(node.internals.positionAbsolute, node.data.element.position)) {
|
|
581
|
+
movedNodes.add(node.id)
|
|
582
|
+
}
|
|
583
|
+
const rect = acc[node.data.fqn] = {
|
|
584
|
+
isCompound: node.data.element.children.length > 0,
|
|
585
|
+
x: Math.floor(node.internals.positionAbsolute.x),
|
|
586
|
+
y: Math.floor(node.internals.positionAbsolute.y),
|
|
587
|
+
width: Math.ceil(dimensions.width),
|
|
588
|
+
height: Math.ceil(dimensions.height)
|
|
589
|
+
}
|
|
590
|
+
bounds = getBoundsOfRects(bounds, rect)
|
|
591
|
+
return acc
|
|
592
|
+
}, {} as Changes.SaveManualLayout['layout']['nodes'])
|
|
593
|
+
const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
|
|
594
|
+
let controlPoints = data.controlPoints
|
|
595
|
+
const sourceOrTargetMoved = movedNodes.has(source) || movedNodes.has(target)
|
|
596
|
+
// If edge control points are not set, but the source or target node was moved
|
|
597
|
+
if ((!controlPoints || controlPoints.length === 0) && sourceOrTargetMoved) {
|
|
598
|
+
controlPoints = bezierControlPoints(data.edge)
|
|
599
|
+
}
|
|
600
|
+
if (data.edge.points.length === 0 && (!controlPoints || controlPoints.length === 0)) {
|
|
493
601
|
return acc
|
|
494
|
-
}, {} as Changes.SaveManualLayout['edges'])
|
|
495
|
-
if (movedNodes.size === 0 && isEmpty(edges)) {
|
|
496
|
-
DEV && console.debug('ignore triggerSaveManualLayout, as no changes detected')
|
|
497
|
-
return
|
|
498
602
|
}
|
|
603
|
+
const _updated: Changes.SaveManualLayout['layout']['edges'][string] = acc[data.edge.id] = {
|
|
604
|
+
points: data.edge.points
|
|
605
|
+
}
|
|
606
|
+
if (data.label?.bbox) {
|
|
607
|
+
_updated.labelBBox = data.label?.bbox
|
|
608
|
+
}
|
|
609
|
+
if (data.edge.labelBBox) {
|
|
610
|
+
_updated.labelBBox ??= data.edge.labelBBox
|
|
611
|
+
}
|
|
612
|
+
if (controlPoints && hasAtLeast(controlPoints, 1)) {
|
|
613
|
+
_updated.controlPoints = controlPoints
|
|
614
|
+
}
|
|
615
|
+
if (!sourceOrTargetMoved && data.edge.dotpos) {
|
|
616
|
+
_updated.dotpos = data.edge.dotpos
|
|
617
|
+
}
|
|
618
|
+
const allX = [...data.edge.points.map(p => p[0]), ...(controlPoints ?? []).map(p => p.x)]
|
|
619
|
+
const allY = [...data.edge.points.map(p => p[1]), ...(controlPoints ?? []).map(p => p.y)]
|
|
620
|
+
const rect = boxToRect({
|
|
621
|
+
x: Math.floor(Math.min(...allX)),
|
|
622
|
+
y: Math.floor(Math.min(...allY)),
|
|
623
|
+
x2: Math.ceil(Math.max(...allX)),
|
|
624
|
+
y2: Math.ceil(Math.max(...allY))
|
|
625
|
+
})
|
|
626
|
+
bounds = getBoundsOfRects(bounds, rect)
|
|
627
|
+
return acc
|
|
628
|
+
}, {} as Changes.SaveManualLayout['layout']['edges'])
|
|
629
|
+
|
|
630
|
+
const change: Changes.SaveManualLayout = {
|
|
631
|
+
op: 'save-manual-layout',
|
|
632
|
+
layout: {
|
|
633
|
+
hash: view.hash,
|
|
634
|
+
autoLayout: view.autoLayout,
|
|
499
635
|
|
|
500
|
-
const change: Changes.SaveManualLayout = {
|
|
501
|
-
op: 'save-manual-layout',
|
|
502
636
|
nodes,
|
|
503
|
-
edges
|
|
637
|
+
edges,
|
|
638
|
+
...bounds
|
|
504
639
|
}
|
|
640
|
+
}
|
|
505
641
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
642
|
+
if (DEV) {
|
|
643
|
+
console.debug('triggerSaveManualLayout', change)
|
|
644
|
+
}
|
|
509
645
|
|
|
510
|
-
|
|
511
|
-
|
|
646
|
+
onChange({ change })
|
|
647
|
+
},
|
|
648
|
+
scheduleSaveManualLayout: () => {
|
|
649
|
+
let { viewSyncDebounceTimeout, onChange } = get()
|
|
650
|
+
if (!onChange) {
|
|
651
|
+
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
if (viewSyncDebounceTimeout) {
|
|
655
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
656
|
+
}
|
|
512
657
|
set(
|
|
513
658
|
{
|
|
514
|
-
viewSyncDebounceTimeout:
|
|
659
|
+
viewSyncDebounceTimeout: setTimeout(() => {
|
|
660
|
+
get().triggerSaveManualLayout()
|
|
661
|
+
}, 1000) as any as number // explicit typecast to number to suppress TS error in astro build
|
|
515
662
|
},
|
|
516
663
|
noReplace,
|
|
517
664
|
'debounce sync state'
|
|
@@ -519,46 +666,90 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
519
666
|
},
|
|
520
667
|
|
|
521
668
|
triggerOnNavigateTo: (xynodeId, event) => {
|
|
522
|
-
const {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
669
|
+
const { view, xynodes, onNavigateTo } = get()
|
|
670
|
+
if (!onNavigateTo) {
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
const xynode = xynodes.find(({ id }) => id === xynodeId)
|
|
674
|
+
invariant(xynode, `xynode not found: ${xynodeId}`)
|
|
675
|
+
const element = view.nodes.find(({ id }) => id === xynodeId)
|
|
676
|
+
invariant(element?.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
527
677
|
set(
|
|
528
678
|
{
|
|
529
679
|
lastClickedNodeId: xynodeId,
|
|
530
680
|
lastOnNavigate: {
|
|
531
681
|
fromView: view.id,
|
|
532
682
|
toView: element.navigateTo,
|
|
533
|
-
|
|
534
|
-
elementScreenPosition: xyflow.flowToScreenPosition({
|
|
535
|
-
x: xynode.internals.positionAbsolute.x, // + dimensions.width / 2,
|
|
536
|
-
y: xynode.internals.positionAbsolute.y // + dimensions.height / 2
|
|
537
|
-
}),
|
|
538
|
-
positionCorrected: false
|
|
683
|
+
fromNode: element.id
|
|
539
684
|
}
|
|
540
685
|
},
|
|
541
686
|
noReplace,
|
|
542
687
|
'triggerOnNavigateTo'
|
|
543
688
|
)
|
|
544
|
-
|
|
689
|
+
onNavigateTo(
|
|
545
690
|
element.navigateTo,
|
|
546
691
|
event,
|
|
547
|
-
element,
|
|
548
|
-
xynode
|
|
692
|
+
element as DiagramNodeWithNavigate,
|
|
693
|
+
xynode
|
|
549
694
|
)
|
|
550
695
|
},
|
|
551
696
|
|
|
697
|
+
goBack: () => {
|
|
698
|
+
const { navigationHistory, navigationHistoryIndex, onNavigateTo } = get()
|
|
699
|
+
const { viewId, nodeId } = nonNullable(navigationHistory[navigationHistoryIndex])
|
|
700
|
+
const stepBack = (navigationHistoryIndex > 0 && navigationHistory[navigationHistoryIndex - 1]) || null
|
|
701
|
+
if (stepBack && onNavigateTo) {
|
|
702
|
+
set(
|
|
703
|
+
{
|
|
704
|
+
lastClickedEdgeId: null,
|
|
705
|
+
lastClickedNodeId: null,
|
|
706
|
+
navigationHistoryIndex: navigationHistoryIndex - 1,
|
|
707
|
+
lastOnNavigate: nodeId
|
|
708
|
+
? {
|
|
709
|
+
fromView: viewId,
|
|
710
|
+
toView: stepBack.viewId,
|
|
711
|
+
fromNode: nodeId as Fqn
|
|
712
|
+
}
|
|
713
|
+
: null
|
|
714
|
+
},
|
|
715
|
+
noReplace,
|
|
716
|
+
'goBack'
|
|
717
|
+
)
|
|
718
|
+
onNavigateTo(stepBack.viewId)
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
goForward: () => {
|
|
722
|
+
const { navigationHistory, navigationHistoryIndex, onNavigateTo } = get()
|
|
723
|
+
const { viewId } = nonNullable(navigationHistory[navigationHistoryIndex])
|
|
724
|
+
const stepForward = navigationHistoryIndex < navigationHistory.length - 1
|
|
725
|
+
? navigationHistory[navigationHistoryIndex + 1]
|
|
726
|
+
: null
|
|
727
|
+
if (stepForward && onNavigateTo) {
|
|
728
|
+
set(
|
|
729
|
+
{
|
|
730
|
+
lastClickedEdgeId: null,
|
|
731
|
+
lastClickedNodeId: null,
|
|
732
|
+
navigationHistoryIndex: navigationHistoryIndex + 1,
|
|
733
|
+
lastOnNavigate: stepForward.nodeId
|
|
734
|
+
? {
|
|
735
|
+
fromView: viewId,
|
|
736
|
+
toView: stepForward.viewId,
|
|
737
|
+
fromNode: stepForward.nodeId as Fqn
|
|
738
|
+
}
|
|
739
|
+
: null
|
|
740
|
+
},
|
|
741
|
+
noReplace,
|
|
742
|
+
'goForward'
|
|
743
|
+
)
|
|
744
|
+
onNavigateTo(stepForward.viewId)
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
|
|
552
748
|
fitDiagram: (duration = 500) => {
|
|
553
749
|
const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get()
|
|
554
750
|
const { width, height, panZoom, transform } = xystore.getState()
|
|
555
751
|
|
|
556
|
-
const bounds =
|
|
557
|
-
x: 0,
|
|
558
|
-
y: 0,
|
|
559
|
-
width: view.width,
|
|
560
|
-
height: view.height
|
|
561
|
-
}
|
|
752
|
+
const bounds = view.bounds
|
|
562
753
|
const maxZoom = Math.max(1, transform[2])
|
|
563
754
|
const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
|
|
564
755
|
panZoom?.setViewport(viewport, { duration })
|
|
@@ -576,12 +767,19 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
576
767
|
},
|
|
577
768
|
|
|
578
769
|
nextDynamicStep: (increment = 1) => {
|
|
579
|
-
const {
|
|
580
|
-
invariant(view.__ === 'dynamic', 'view is not dynamic')
|
|
770
|
+
const { activeDynamicViewStep, activateDynamicStep } = get()
|
|
581
771
|
const nextStep = (activeDynamicViewStep ?? 0) + increment
|
|
582
|
-
if (nextStep <= 0
|
|
772
|
+
if (nextStep <= 0) {
|
|
583
773
|
return
|
|
584
774
|
}
|
|
775
|
+
if (nextStep !== activeDynamicViewStep) {
|
|
776
|
+
activateDynamicStep(nextStep)
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
activateDynamicStep: (nextStep: number) => {
|
|
781
|
+
const { isDynamicView, xyflow, xystore, fitViewPadding } = get()
|
|
782
|
+
invariant(isDynamicView, 'view is not dynamic')
|
|
585
783
|
const edgeId = StepEdgeId(nextStep)
|
|
586
784
|
const dimmed = new StringSet()
|
|
587
785
|
let edge: XYFlowEdge | null = null
|
|
@@ -617,7 +815,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
617
815
|
dimmed
|
|
618
816
|
},
|
|
619
817
|
noReplace,
|
|
620
|
-
'
|
|
818
|
+
'activateDynamicStep'
|
|
621
819
|
)
|
|
622
820
|
},
|
|
623
821
|
|
|
@@ -634,6 +832,20 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
634
832
|
)
|
|
635
833
|
get().fitDiagram()
|
|
636
834
|
}
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
onInit: (instance) => {
|
|
838
|
+
const { xyflow, initialized } = get()
|
|
839
|
+
if (!initialized || xyflow !== instance) {
|
|
840
|
+
set(
|
|
841
|
+
{
|
|
842
|
+
xyflow: instance,
|
|
843
|
+
initialized: true
|
|
844
|
+
},
|
|
845
|
+
noReplace,
|
|
846
|
+
'onInit'
|
|
847
|
+
)
|
|
848
|
+
}
|
|
637
849
|
}
|
|
638
850
|
}),
|
|
639
851
|
{
|