@likec4/diagram 1.2.2 → 1.3.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 +21 -3
- package/dist/index.mjs +1335 -1240
- package/dist/state/DiagramContext.d.ts +2263 -175
- package/dist/state/diagramStore.d.ts +2272 -180
- package/dist/state/useDiagramStore.d.ts +2409 -321
- package/dist/style.css +123 -92
- package/dist/theme.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +3 -1
- package/dist/xyflow/XYFlow.d.ts +2 -1
- package/dist/xyflow/XYFlowInner.d.ts +1 -1
- package/dist/xyflow/const.d.ts +1 -1
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -4
- package/dist/xyflow/edges/utils.d.ts +4 -2
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -1
- package/dist/xyflow/types.d.ts +2 -2
- package/dist/xyflow/utils.d.ts +3 -0
- package/package.json +15 -14
- package/src/LikeC4Diagram.css.ts +4 -4
- package/src/LikeC4Diagram.props.ts +23 -2
- package/src/LikeC4Diagram.tsx +79 -31
- package/src/globals.css.ts +0 -1
- package/src/mantine/theme.ts +1 -0
- package/src/state/DiagramContext.tsx +0 -1
- package/src/state/diagramStore.ts +177 -100
- package/src/theme.css.ts +0 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -3
- package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
- package/src/ui/DynamicViewWalkthrough.tsx +36 -17
- package/src/ui/OptionsPanel.tsx +12 -12
- package/src/ui/options/NodeOptions.tsx +16 -19
- package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
- package/src/xyflow/SyncWithDiagram.tsx +21 -2
- package/src/xyflow/XYFlow.tsx +20 -23
- package/src/xyflow/XYFlowEvents.tsx +26 -8
- package/src/xyflow/XYFlowInner.tsx +5 -4
- package/src/xyflow/const.ts +1 -1
- package/src/xyflow/diagram-to-xyflow.ts +5 -31
- package/src/xyflow/edges/RelationshipEdge.tsx +158 -112
- package/src/xyflow/edges/edges.css.ts +32 -21
- package/src/xyflow/edges/utils.ts +5 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +17 -3
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +40 -32
- package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
- package/src/xyflow/nodes/element/ElementNode.tsx +20 -6
- package/src/xyflow/nodes/element/element.css.ts +15 -15
- package/src/xyflow/nodes/element/index.ts +1 -1
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
- package/src/xyflow/types.ts +5 -2
- package/src/xyflow/utils.ts +48 -0
- package/dist/ui/index.d.ts +0 -2
- package/src/ui/index.ts +0 -3
|
@@ -3,12 +3,14 @@ import type {
|
|
|
3
3
|
DiagramEdge,
|
|
4
4
|
DiagramNode,
|
|
5
5
|
DiagramView,
|
|
6
|
+
EdgeId,
|
|
6
7
|
ElementShape,
|
|
7
8
|
Fqn,
|
|
8
9
|
NonEmptyArray,
|
|
9
10
|
ThemeColor,
|
|
10
11
|
ViewID
|
|
11
12
|
} from '@likec4/core'
|
|
13
|
+
import type { XYPosition } from '@xyflow/system'
|
|
12
14
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
13
15
|
import type { SetRequired, Simplify } from 'type-fest'
|
|
14
16
|
import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
|
|
@@ -55,12 +57,25 @@ export namespace Changes {
|
|
|
55
57
|
}
|
|
56
58
|
targets: NonEmptyArray<Fqn>
|
|
57
59
|
}
|
|
60
|
+
|
|
61
|
+
export interface SaveManualLayout {
|
|
62
|
+
op: 'save-manual-layout'
|
|
63
|
+
nodes: Record<Fqn, {
|
|
64
|
+
x: number
|
|
65
|
+
y: number
|
|
66
|
+
width: number
|
|
67
|
+
height: number
|
|
68
|
+
}>
|
|
69
|
+
edges: Record<EdgeId, {
|
|
70
|
+
controlPoints: XYPosition[]
|
|
71
|
+
}>
|
|
72
|
+
}
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
export type Change = Changes.ChangeElementStyle
|
|
75
|
+
export type Change = Changes.ChangeElementStyle | Changes.SaveManualLayout
|
|
61
76
|
|
|
62
77
|
export type ChangeEvent = {
|
|
63
|
-
|
|
78
|
+
change: Change
|
|
64
79
|
}
|
|
65
80
|
export type OnChange = {
|
|
66
81
|
(event: ChangeEvent): void
|
|
@@ -149,6 +164,12 @@ export interface LikeC4DiagramProperties {
|
|
|
149
164
|
* @default false
|
|
150
165
|
*/
|
|
151
166
|
enableDynamicViewWalkthrough?: boolean | undefined
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Experimental feature to enable edge editing
|
|
170
|
+
* @default false
|
|
171
|
+
*/
|
|
172
|
+
experimentalEdgeEditing?: boolean | undefined
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
export interface LikeC4DiagramEventHandlers {
|
package/src/LikeC4Diagram.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import '@xyflow/react/dist/style.css'
|
|
2
1
|
import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
|
|
2
|
+
import '@xyflow/react/dist/style.css'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
|
-
import {
|
|
4
|
+
import { DEV } from 'esm-env'
|
|
5
|
+
import { shallowEqual } from 'fast-equals'
|
|
6
|
+
import { domAnimation, LazyMotion } from 'framer-motion'
|
|
7
|
+
import { memo, useRef } from 'react'
|
|
8
|
+
import useTilg from 'tilg'
|
|
5
9
|
import { rootClassName } from './globals.css'
|
|
6
10
|
import { KeepAspectRatioContainer } from './KeepAspectRatioContainer'
|
|
7
11
|
import { cssDisablePan, cssNoControls, cssReactFlow, cssTransparentBg } from './LikeC4Diagram.css'
|
|
@@ -13,7 +17,7 @@ import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
|
|
|
13
17
|
import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
|
|
14
18
|
import { SelectEdgesOnNodeFocus } from './xyflow/SelectEdgesOnNodeFocus'
|
|
15
19
|
import { SyncWithDiagram } from './xyflow/SyncWithDiagram'
|
|
16
|
-
import type { XYFlowData } from './xyflow/types'
|
|
20
|
+
import type { XYFlowData, XYFlowEdge, XYFlowNode } from './xyflow/types'
|
|
17
21
|
import { XYFlow } from './xyflow/XYFlow'
|
|
18
22
|
import { XYFlowInner } from './xyflow/XYFlowInner'
|
|
19
23
|
|
|
@@ -29,13 +33,14 @@ export function LikeC4Diagram({
|
|
|
29
33
|
nodesSelectable = !readonly,
|
|
30
34
|
nodesDraggable = !readonly,
|
|
31
35
|
background = 'dots',
|
|
32
|
-
controls =
|
|
36
|
+
controls = false,
|
|
33
37
|
showElementLinks = true,
|
|
34
38
|
showDiagramTitle = true,
|
|
35
39
|
enableDynamicViewWalkthrough = true,
|
|
36
40
|
initialWidth,
|
|
37
41
|
initialHeight,
|
|
38
42
|
keepAspectRatio = false,
|
|
43
|
+
experimentalEdgeEditing = false,
|
|
39
44
|
onCanvasClick,
|
|
40
45
|
onCanvasContextMenu,
|
|
41
46
|
onCanvasDblClick,
|
|
@@ -46,7 +51,7 @@ export function LikeC4Diagram({
|
|
|
46
51
|
onNodeClick,
|
|
47
52
|
onNodeContextMenu
|
|
48
53
|
}: LikeC4DiagramProps) {
|
|
49
|
-
|
|
54
|
+
DEV && useTilg()
|
|
50
55
|
const initialRef = useRef<{
|
|
51
56
|
defaultNodes: XYFlowData['nodes']
|
|
52
57
|
defaultEdges: XYFlowData['edges']
|
|
@@ -81,6 +86,7 @@ export function LikeC4Diagram({
|
|
|
81
86
|
showElementLinks={showElementLinks}
|
|
82
87
|
nodesDraggable={nodesDraggable}
|
|
83
88
|
nodesSelectable={nodesSelectable}
|
|
89
|
+
experimentalEdgeEditing={experimentalEdgeEditing}
|
|
84
90
|
onCanvasClick={onCanvasClick}
|
|
85
91
|
onCanvasContextMenu={onCanvasContextMenu}
|
|
86
92
|
onEdgeClick={onEdgeClick}
|
|
@@ -91,38 +97,80 @@ export function LikeC4Diagram({
|
|
|
91
97
|
onNavigateTo={onNavigateTo}
|
|
92
98
|
onCanvasDblClick={onCanvasDblClick}
|
|
93
99
|
>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<XYFlow
|
|
101
|
-
className={clsx(
|
|
102
|
-
'likec4-diagram',
|
|
103
|
-
cssReactFlow,
|
|
104
|
-
controls === false && cssNoControls,
|
|
105
|
-
pannable !== true && cssDisablePan,
|
|
106
|
-
background === 'transparent' && cssTransparentBg
|
|
107
|
-
)}
|
|
108
|
-
defaultNodes={initialRef.current.defaultNodes}
|
|
109
|
-
defaultEdges={initialRef.current.defaultEdges}
|
|
100
|
+
<LazyMotion features={domAnimation} strict>
|
|
101
|
+
<KeepAspectRatioContainer
|
|
102
|
+
className={clsx(rootClassName, className)}
|
|
103
|
+
enabled={keepAspectRatio}
|
|
104
|
+
width={view.width}
|
|
105
|
+
height={view.height}
|
|
110
106
|
>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
<LikeC4DiagramInnerMemo
|
|
108
|
+
defaultNodes={initialRef.current.defaultNodes}
|
|
109
|
+
defaultEdges={initialRef.current.defaultEdges}
|
|
110
|
+
fitView={fitView}
|
|
111
|
+
zoomable={zoomable}
|
|
114
112
|
background={background}
|
|
115
113
|
controls={controls}
|
|
114
|
+
pannable={pannable}
|
|
115
|
+
showDiagramTitle={showDiagramTitle}
|
|
116
|
+
enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
|
|
116
117
|
/>
|
|
117
|
-
</
|
|
118
|
-
|
|
119
|
-
<SyncWithDiagram />
|
|
120
|
-
{fitView && <FitViewOnDiagramChange />}
|
|
121
|
-
{fitView && zoomable && <SelectEdgesOnNodeFocus />}
|
|
122
|
-
</WhenInitialized>
|
|
123
|
-
</KeepAspectRatioContainer>
|
|
118
|
+
</KeepAspectRatioContainer>
|
|
119
|
+
</LazyMotion>
|
|
124
120
|
</DiagramContextProvider>
|
|
125
121
|
</XYFlowProvider>
|
|
126
122
|
</EnsureMantine>
|
|
127
123
|
)
|
|
128
124
|
}
|
|
125
|
+
|
|
126
|
+
type LikeC4DiagramInnerProps = {
|
|
127
|
+
background: NonNullable<LikeC4DiagramProperties['background']>
|
|
128
|
+
fitView: boolean
|
|
129
|
+
zoomable: boolean
|
|
130
|
+
pannable: boolean
|
|
131
|
+
controls: boolean
|
|
132
|
+
defaultNodes: XYFlowNode[]
|
|
133
|
+
defaultEdges: XYFlowEdge[]
|
|
134
|
+
showDiagramTitle: boolean
|
|
135
|
+
enableDynamicViewWalkthrough: boolean
|
|
136
|
+
}
|
|
137
|
+
const LikeC4DiagramInnerMemo = memo<LikeC4DiagramInnerProps>(function LikeC4DiagramInner({
|
|
138
|
+
background,
|
|
139
|
+
fitView,
|
|
140
|
+
zoomable,
|
|
141
|
+
controls,
|
|
142
|
+
pannable,
|
|
143
|
+
defaultNodes,
|
|
144
|
+
defaultEdges,
|
|
145
|
+
showDiagramTitle,
|
|
146
|
+
enableDynamicViewWalkthrough
|
|
147
|
+
}) {
|
|
148
|
+
DEV && useTilg()
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
<XYFlow
|
|
152
|
+
defaultNodes={defaultNodes}
|
|
153
|
+
defaultEdges={defaultEdges}
|
|
154
|
+
className={clsx(
|
|
155
|
+
'likec4-diagram',
|
|
156
|
+
cssReactFlow,
|
|
157
|
+
controls === false && cssNoControls,
|
|
158
|
+
pannable !== true && cssDisablePan,
|
|
159
|
+
background === 'transparent' && cssTransparentBg
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<XYFlowInner
|
|
163
|
+
showDiagramTitle={showDiagramTitle}
|
|
164
|
+
enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
|
|
165
|
+
background={background}
|
|
166
|
+
controls={controls}
|
|
167
|
+
/>
|
|
168
|
+
</XYFlow>
|
|
169
|
+
<WhenInitialized>
|
|
170
|
+
<SyncWithDiagram />
|
|
171
|
+
{fitView && <FitViewOnDiagramChange />}
|
|
172
|
+
{fitView && zoomable && <SelectEdgesOnNodeFocus />}
|
|
173
|
+
</WhenInitialized>
|
|
174
|
+
</>
|
|
175
|
+
)
|
|
176
|
+
}, shallowEqual)
|
package/src/globals.css.ts
CHANGED
package/src/mantine/theme.ts
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
DiagramEdge,
|
|
4
|
-
DiagramNode,
|
|
5
|
-
DiagramView,
|
|
6
|
-
ElementShape,
|
|
7
|
-
Fqn,
|
|
8
|
-
NonEmptyArray,
|
|
9
|
-
ThemeColor,
|
|
10
|
-
ViewID
|
|
11
|
-
} from '@likec4/core'
|
|
12
|
-
import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
|
|
1
|
+
import type { DiagramNode, DiagramView, Fqn, ViewID } from '@likec4/core'
|
|
2
|
+
import { invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
|
|
13
3
|
import { getViewportForBounds, type XYPosition } from '@xyflow/react'
|
|
14
|
-
import { getNodeDimensions } from '@xyflow/system'
|
|
4
|
+
import { getInternalNodesBounds, getNodeDimensions } from '@xyflow/system'
|
|
15
5
|
import { DEV } from 'esm-env'
|
|
16
6
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
17
7
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
18
|
-
import {
|
|
8
|
+
import { debounce, entries, mapToObj, reduce } from 'remeda'
|
|
19
9
|
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
20
10
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
21
11
|
import { shallow } from 'zustand/shallow'
|
|
22
12
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
23
|
-
import type {
|
|
13
|
+
import type { Changes, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
|
|
24
14
|
import { MinZoom } from '../xyflow/const'
|
|
25
15
|
import type { XYStoreApi } from '../xyflow/hooks'
|
|
26
16
|
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
17
|
+
import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
|
|
27
18
|
|
|
28
19
|
export type DiagramStore = {
|
|
29
20
|
// Incoming props
|
|
@@ -36,8 +27,12 @@ export type DiagramStore = {
|
|
|
36
27
|
pannable: boolean
|
|
37
28
|
nodesDraggable: boolean
|
|
38
29
|
nodesSelectable: boolean
|
|
30
|
+
experimentalEdgeEditing: boolean
|
|
39
31
|
|
|
40
32
|
// Internal state
|
|
33
|
+
viewSyncState: 'synced' | 'changed'
|
|
34
|
+
viewSyncDebounceTimeout: number | null
|
|
35
|
+
|
|
41
36
|
xyflow: XYFlowInstance
|
|
42
37
|
initialized: boolean
|
|
43
38
|
xyflowSynced: boolean
|
|
@@ -62,7 +57,8 @@ export type DiagramStore = {
|
|
|
62
57
|
fromView: ViewID
|
|
63
58
|
toView: ViewID
|
|
64
59
|
element: DiagramNode
|
|
65
|
-
|
|
60
|
+
// Position of the element in the source view
|
|
61
|
+
elementScreenPosition: XYPosition
|
|
66
62
|
// If node from the target view is placed on the same position as the node from the source view
|
|
67
63
|
positionCorrected: boolean
|
|
68
64
|
}
|
|
@@ -85,6 +81,7 @@ export type DiagramInitialState = // Required properties
|
|
|
85
81
|
| 'pannable'
|
|
86
82
|
| 'nodesDraggable'
|
|
87
83
|
| 'nodesSelectable'
|
|
84
|
+
| 'experimentalEdgeEditing'
|
|
88
85
|
>
|
|
89
86
|
// Optional properties
|
|
90
87
|
// & Partial<
|
|
@@ -111,7 +108,8 @@ interface DiagramStoreActions {
|
|
|
111
108
|
resetLastClicked: () => void
|
|
112
109
|
|
|
113
110
|
getElement(id: Fqn): DiagramNode | null
|
|
114
|
-
|
|
111
|
+
triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
|
|
112
|
+
triggerSaveManualLayout: (xystore: XYStoreApi) => void
|
|
115
113
|
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
116
114
|
fitDiagram: (xyStore: XYStoreApi) => void
|
|
117
115
|
|
|
@@ -122,6 +120,8 @@ interface DiagramStoreActions {
|
|
|
122
120
|
export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
|
|
123
121
|
|
|
124
122
|
const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
|
|
123
|
+
viewSyncState: 'synced',
|
|
124
|
+
viewSyncDebounceTimeout: null,
|
|
125
125
|
initialized: false,
|
|
126
126
|
xyflowSynced: false,
|
|
127
127
|
previousViews: [],
|
|
@@ -173,6 +173,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
173
173
|
|
|
174
174
|
updateView: (nextView) => {
|
|
175
175
|
let {
|
|
176
|
+
viewSyncDebounceTimeout,
|
|
177
|
+
viewSyncState,
|
|
178
|
+
xyflow,
|
|
176
179
|
dimmed,
|
|
177
180
|
xyflowSynced,
|
|
178
181
|
view: current,
|
|
@@ -186,9 +189,21 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
186
189
|
hoveredNodeId
|
|
187
190
|
} = get()
|
|
188
191
|
|
|
192
|
+
if (viewSyncDebounceTimeout) {
|
|
193
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
194
|
+
viewSyncDebounceTimeout = null
|
|
195
|
+
}
|
|
196
|
+
|
|
189
197
|
if (shallowEqual(current, nextView)) {
|
|
190
|
-
if (!xyflowSynced) {
|
|
191
|
-
set(
|
|
198
|
+
if (!xyflowSynced || viewSyncState === 'changed') {
|
|
199
|
+
set(
|
|
200
|
+
{
|
|
201
|
+
viewSyncState: 'synced',
|
|
202
|
+
xyflowSynced: true
|
|
203
|
+
},
|
|
204
|
+
noReplace,
|
|
205
|
+
'updateView: xyflow synced'
|
|
206
|
+
)
|
|
192
207
|
}
|
|
193
208
|
DEV && console.debug('store: skip updateView')
|
|
194
209
|
return
|
|
@@ -218,8 +233,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
218
233
|
if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
|
|
219
234
|
activeDynamicViewStep = null
|
|
220
235
|
}
|
|
221
|
-
xyflowSynced = deepEqual(current.nodes,
|
|
222
|
-
if (
|
|
236
|
+
xyflowSynced = deepEqual([current.nodes, current.edges], [nextView.nodes, nextView.edges])
|
|
237
|
+
if (dimmed.size > 0) {
|
|
223
238
|
let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
|
|
224
239
|
if (nextDimmed.size !== dimmed.size) {
|
|
225
240
|
dimmed = nextDimmed
|
|
@@ -233,6 +248,11 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
233
248
|
}
|
|
234
249
|
}
|
|
235
250
|
|
|
251
|
+
if (!lastOnNavigate) {
|
|
252
|
+
const zoom = xyflow.getZoom()
|
|
253
|
+
xyflow.setCenter(nextView.width / 2, nextView.height / 2, { zoom })
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
// Reset hovered / clicked node/edge if the view is different
|
|
237
257
|
lastClickedEdgeId = null
|
|
238
258
|
lastClickedNodeId = null
|
|
@@ -252,6 +272,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
252
272
|
|
|
253
273
|
set(
|
|
254
274
|
{
|
|
275
|
+
viewSyncState: 'synced',
|
|
276
|
+
viewSyncDebounceTimeout,
|
|
255
277
|
view: nextView,
|
|
256
278
|
activeDynamicViewStep,
|
|
257
279
|
xyflowSynced,
|
|
@@ -326,13 +348,15 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
326
348
|
|
|
327
349
|
resetLastClicked: () => {
|
|
328
350
|
let {
|
|
351
|
+
activeDynamicViewStep,
|
|
329
352
|
focusedNodeId,
|
|
330
353
|
lastClickedNodeId,
|
|
331
354
|
lastClickedEdgeId
|
|
332
355
|
} = get()
|
|
333
|
-
if (focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
|
|
356
|
+
if (activeDynamicViewStep || focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
|
|
334
357
|
set(
|
|
335
358
|
{
|
|
359
|
+
activeDynamicViewStep: null,
|
|
336
360
|
focusedNodeId: null,
|
|
337
361
|
lastClickedNodeId: null,
|
|
338
362
|
lastClickedEdgeId: null,
|
|
@@ -349,93 +373,144 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
349
373
|
return view.nodes.find(({ id }) => id === fqn) ?? null
|
|
350
374
|
},
|
|
351
375
|
|
|
352
|
-
|
|
376
|
+
triggerChangeElementStyle: (change) => {
|
|
353
377
|
if (DEV) {
|
|
354
|
-
console.debug('
|
|
378
|
+
console.debug('triggerChangeElementStyle', change)
|
|
355
379
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (change.
|
|
362
|
-
|
|
380
|
+
const { view, updateView, onChange } = get()
|
|
381
|
+
|
|
382
|
+
// Style changes we already apply to the view
|
|
383
|
+
let hasChanges = false
|
|
384
|
+
const nodes = view.nodes.map(origin => {
|
|
385
|
+
if (!change.targets.includes(origin.id)) {
|
|
386
|
+
return origin
|
|
363
387
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
let element = origin
|
|
389
|
+
for (const [key, value] of entries.strict(change.style)) {
|
|
390
|
+
switch (key) {
|
|
391
|
+
case 'shape':
|
|
392
|
+
if (value !== element.shape) {
|
|
393
|
+
element = {
|
|
394
|
+
...element,
|
|
395
|
+
shape: value
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
break
|
|
399
|
+
case 'color':
|
|
400
|
+
if (value !== element.color) {
|
|
401
|
+
element = {
|
|
402
|
+
...element,
|
|
403
|
+
color: value
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break
|
|
407
|
+
case 'opacity':
|
|
408
|
+
if (value !== element.style?.opacity) {
|
|
409
|
+
element = {
|
|
410
|
+
...element,
|
|
411
|
+
style: {
|
|
412
|
+
...element.style,
|
|
413
|
+
opacity: value
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
break
|
|
418
|
+
case 'border':
|
|
419
|
+
if (value !== element.style?.border) {
|
|
420
|
+
element = {
|
|
421
|
+
...element,
|
|
422
|
+
style: {
|
|
423
|
+
...element.style,
|
|
424
|
+
border: value
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
break
|
|
429
|
+
default:
|
|
430
|
+
nonexhaustive(key)
|
|
382
431
|
}
|
|
383
432
|
}
|
|
433
|
+
if (element !== origin) {
|
|
434
|
+
hasChanges = true
|
|
435
|
+
return element
|
|
436
|
+
}
|
|
437
|
+
return origin
|
|
438
|
+
})
|
|
439
|
+
if (hasChanges) {
|
|
440
|
+
updateView({
|
|
441
|
+
...view,
|
|
442
|
+
nodes
|
|
443
|
+
})
|
|
444
|
+
if (onChange) {
|
|
445
|
+
set({ viewSyncState: 'changed' }, noReplace, 'change sync state')
|
|
446
|
+
}
|
|
384
447
|
}
|
|
448
|
+
// Trigger change event, even if there are no changes with local state
|
|
449
|
+
// but we maybe out of sync with the server
|
|
450
|
+
onChange?.({ change })
|
|
451
|
+
},
|
|
385
452
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
453
|
+
triggerSaveManualLayout: (xystore) => {
|
|
454
|
+
let { viewSyncDebounceTimeout: debounced, onChange } = get()
|
|
455
|
+
if (!onChange) {
|
|
456
|
+
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
if (debounced) {
|
|
460
|
+
clearTimeout(debounced)
|
|
461
|
+
}
|
|
462
|
+
debounced = setTimeout(() => {
|
|
463
|
+
const { nodeLookup } = xystore.getState()
|
|
464
|
+
const { xyflow, onChange } = get()
|
|
465
|
+
|
|
466
|
+
const movedNodes = new StringSet()
|
|
467
|
+
const nodes = reduce([...nodeLookup.values()], (acc, node) => {
|
|
468
|
+
const dimensions = getNodeDimensions(node)
|
|
469
|
+
if (!isSamePoint(node.internals.positionAbsolute, node.data.element.position)) {
|
|
470
|
+
movedNodes.add(node.id)
|
|
395
471
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
472
|
+
acc[node.data.fqn] = {
|
|
473
|
+
x: node.internals.positionAbsolute.x,
|
|
474
|
+
y: node.internals.positionAbsolute.y,
|
|
475
|
+
width: dimensions.width,
|
|
476
|
+
height: dimensions.height
|
|
402
477
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
opacity
|
|
411
|
-
}
|
|
412
|
-
}
|
|
478
|
+
return acc
|
|
479
|
+
}, {} as Changes.SaveManualLayout['nodes'])
|
|
480
|
+
const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
|
|
481
|
+
let controlPoints = data.controlPoints
|
|
482
|
+
// If edge control points are not set, but the source or target node was moved
|
|
483
|
+
if (!controlPoints && (movedNodes.has(source) || movedNodes.has(target))) {
|
|
484
|
+
controlPoints = bezierControlPoints(data.edge)
|
|
413
485
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
element = {
|
|
418
|
-
...element,
|
|
419
|
-
style: {
|
|
420
|
-
...element.style,
|
|
421
|
-
border
|
|
422
|
-
}
|
|
486
|
+
if (controlPoints) {
|
|
487
|
+
acc[data.edge.id] = {
|
|
488
|
+
controlPoints
|
|
423
489
|
}
|
|
424
490
|
}
|
|
491
|
+
return acc
|
|
492
|
+
}, {} as Changes.SaveManualLayout['edges'])
|
|
425
493
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
})
|
|
494
|
+
const change: Changes.SaveManualLayout = {
|
|
495
|
+
op: 'save-manual-layout',
|
|
496
|
+
nodes,
|
|
497
|
+
edges
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (DEV) {
|
|
501
|
+
console.debug('triggerSaveManualLayout', change)
|
|
502
|
+
}
|
|
503
|
+
set({ viewSyncDebounceTimeout: null }, noReplace, 'clear debounce timeout')
|
|
504
|
+
onChange?.({ change })
|
|
505
|
+
}, 5000)
|
|
506
|
+
set(
|
|
507
|
+
{
|
|
508
|
+
viewSyncDebounceTimeout: debounced,
|
|
509
|
+
viewSyncState: 'changed'
|
|
510
|
+
},
|
|
511
|
+
noReplace,
|
|
512
|
+
'change sync state'
|
|
437
513
|
)
|
|
438
|
-
get().onChange?.({ changes })
|
|
439
514
|
},
|
|
440
515
|
|
|
441
516
|
triggerOnNavigateTo: (xynodeId, event) => {
|
|
@@ -443,7 +518,6 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
443
518
|
const xynode = xyflow.getInternalNode(xynodeId)
|
|
444
519
|
const element = xynode?.data.element as DiagramNodeWithNavigate
|
|
445
520
|
invariant(!!xynode && element?.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
446
|
-
const dimensions = getNodeDimensions(xynode)
|
|
447
521
|
set(
|
|
448
522
|
{
|
|
449
523
|
lastClickedNodeId: xynodeId,
|
|
@@ -451,9 +525,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
451
525
|
fromView: view.id,
|
|
452
526
|
toView: element.navigateTo,
|
|
453
527
|
element: element,
|
|
454
|
-
|
|
455
|
-
x: xynode.internals.positionAbsolute.x + dimensions.width / 2,
|
|
456
|
-
y: xynode.internals.positionAbsolute.y + dimensions.height / 2
|
|
528
|
+
elementScreenPosition: xyflow.flowToScreenPosition({
|
|
529
|
+
x: xynode.internals.positionAbsolute.x, // + dimensions.width / 2,
|
|
530
|
+
y: xynode.internals.positionAbsolute.y // + dimensions.height / 2
|
|
457
531
|
}),
|
|
458
532
|
positionCorrected: false
|
|
459
533
|
}
|
|
@@ -479,7 +553,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
479
553
|
height: view.height
|
|
480
554
|
}
|
|
481
555
|
const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
|
|
482
|
-
panZoom?.setViewport(viewport, { duration:
|
|
556
|
+
panZoom?.setViewport(viewport, { duration: 500 })
|
|
483
557
|
if (!!focusedNodeId) {
|
|
484
558
|
set(
|
|
485
559
|
{
|
|
@@ -496,6 +570,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
496
570
|
const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
|
|
497
571
|
invariant(view.__ === 'dynamic', 'view is not dynamic')
|
|
498
572
|
const nextStep = (activeDynamicViewStep ?? 0) + increment
|
|
573
|
+
if (nextStep <= 0 || nextStep > view.edges.length) {
|
|
574
|
+
return
|
|
575
|
+
}
|
|
499
576
|
const edgeId = StepEdgeId(nextStep)
|
|
500
577
|
const dimmed = new StringSet()
|
|
501
578
|
let edge: XYFlowEdge | null = null
|