@likec4/diagram 1.16.0 → 1.17.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/package.json +7 -7
- package/src/LikeC4CustomColors.tsx +24 -21
- package/src/LikeC4Diagram.props.ts +9 -9
- package/src/LikeC4Diagram.tsx +25 -26
- package/src/StaticLikeC4Diagram.tsx +9 -5
- package/src/overlays/OverlayContext.tsx +1 -1
- package/src/overlays/Overlays.css.ts +28 -23
- package/src/overlays/Overlays.tsx +116 -64
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
- package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
- package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
- package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
- package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
- package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
- package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
- package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
- package/src/overlays/relationships-of/_types.ts +15 -4
- package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
- package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
- package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
- package/src/state/DiagramContext.tsx +15 -8
- package/src/state/diagramStore.ts +14 -6
- package/src/theme-vars.ts +9 -0
- package/src/ui/FramerMotionConfig.tsx +18 -0
- package/src/ui/notation/NotationPanel.tsx +3 -3
- package/src/xyflow/XYFlow.tsx +18 -35
- package/src/xyflow/XYFlowInner.tsx +2 -2
- package/src/xyflow/edges/EdgeLabel.tsx +3 -3
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
- package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
- package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
- package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
- package/src/xyflow/nodes/element/element.css.ts +38 -18
- package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
- package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
- package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
- package/src/xyflow/utils.ts +12 -3
- package/src/LikeC4CustomColors.props.ts +0 -5
- /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import { type Fqn, isAncestor, type Relation } from '@likec4/core'
|
|
1
|
+
import { delay, type DiagramView, type Fqn, isAncestor, type Relation } from '@likec4/core'
|
|
2
2
|
import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
|
|
3
|
-
import { useLocalStorage, useStateHistory } from '@mantine/hooks'
|
|
4
|
-
import { useDebouncedCallback, useSyncedRef } from '@react-hookz/web'
|
|
3
|
+
import { useId, useLocalStorage, useStateHistory } from '@mantine/hooks'
|
|
4
|
+
import { useDebouncedCallback, useDeepCompareEffect, useSyncedRef } from '@react-hookz/web'
|
|
5
5
|
import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
|
|
6
6
|
import {
|
|
7
|
+
Background,
|
|
8
|
+
BackgroundVariant,
|
|
7
9
|
getViewportForBounds,
|
|
8
10
|
Panel,
|
|
9
11
|
ReactFlow,
|
|
10
12
|
type ReactFlowInstance,
|
|
13
|
+
type ReactFlowProps,
|
|
14
|
+
ReactFlowProvider,
|
|
15
|
+
type ReactFlowState,
|
|
16
|
+
useOnViewportChange,
|
|
11
17
|
useReactFlow,
|
|
18
|
+
useStore,
|
|
12
19
|
useStoreApi
|
|
13
20
|
} from '@xyflow/react'
|
|
14
21
|
import { getNodeDimensions } from '@xyflow/system'
|
|
15
|
-
import { memo, useEffect, useRef } from 'react'
|
|
16
|
-
import { map, only, prop, unique } from 'remeda'
|
|
22
|
+
import { memo, type PropsWithChildren, useEffect, useRef, useState } from 'react'
|
|
23
|
+
import { isNullish, map, omit, only, prop, setPath, unique } from 'remeda'
|
|
24
|
+
import { useUpdateEffect } from '../../hooks'
|
|
17
25
|
import { useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
26
|
+
import { centerXYInternalNode } from '../../xyflow/utils'
|
|
18
27
|
import { useOverlayDialog } from '../OverlayContext'
|
|
28
|
+
import { cssReactflowMarker } from '../Overlays.css'
|
|
19
29
|
import type { XYFlowTypes } from './_types'
|
|
20
30
|
import { SelectElement } from './SelectElement'
|
|
21
31
|
import { useLayoutedRelationships, ZIndexes } from './use-layouted-relationships'
|
|
@@ -33,8 +43,11 @@ const edgeTypes = {
|
|
|
33
43
|
relation: RelationshipEdge
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Root node in 'subjects' column
|
|
48
|
+
*/
|
|
49
|
+
const findRootSubject = (nodes: XYFlowTypes.Node[]) =>
|
|
50
|
+
nodes.find((n): n is XYFlowTypes.ElementNode => n.data.column === 'subjects' && isNullish(n.parentId))
|
|
38
51
|
|
|
39
52
|
const resetDimmedAndHovered = (xyflow: ReactFlowInstance<XYFlowTypes.Node, XYFlowTypes.Edge>) => {
|
|
40
53
|
xyflow.setEdges(edges =>
|
|
@@ -80,59 +93,90 @@ const onlyOneUnique = <T extends keyof Relation>(
|
|
|
80
93
|
return only(unique(map(data.relations, prop(property))))
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
type RelationshipsXYFlowProps =
|
|
97
|
+
& PropsWithChildren<{
|
|
98
|
+
subjectId: Fqn
|
|
99
|
+
view: DiagramView
|
|
100
|
+
nodes: XYFlowTypes.Node[]
|
|
101
|
+
edges: XYFlowTypes.Edge[]
|
|
102
|
+
bounds: {
|
|
103
|
+
x: number
|
|
104
|
+
y: number
|
|
105
|
+
width: number
|
|
106
|
+
height: number
|
|
107
|
+
}
|
|
108
|
+
viewportPadding?: number | undefined
|
|
109
|
+
}>
|
|
110
|
+
& Pick<
|
|
111
|
+
ReactFlowProps<XYFlowTypes.Node, XYFlowTypes.Edge>,
|
|
112
|
+
| 'onNodeClick'
|
|
113
|
+
| 'elementsSelectable'
|
|
114
|
+
| 'maxZoom'
|
|
115
|
+
| 'minZoom'
|
|
116
|
+
>
|
|
117
|
+
const selectContainerViewport = (s: ReactFlowState) => `${s.width}x${s.height}`
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
function RelationshipsXYFlowWrapped({
|
|
120
|
+
subjectId,
|
|
121
|
+
view,
|
|
122
|
+
nodes,
|
|
123
|
+
edges,
|
|
124
|
+
bounds,
|
|
125
|
+
children,
|
|
126
|
+
maxZoom = 2,
|
|
127
|
+
minZoom = 0.05,
|
|
128
|
+
viewportPadding = 0.1,
|
|
129
|
+
...rest
|
|
130
|
+
}: RelationshipsXYFlowProps) {
|
|
131
|
+
const id = useId()
|
|
132
|
+
|
|
133
|
+
const lastClickedNodeRef = useRef<XYFlowTypes.NonEmptyNode | null>(null)
|
|
103
134
|
|
|
104
135
|
const xyflow = useReactFlow<XYFlowTypes.Node, XYFlowTypes.Edge>()
|
|
105
136
|
const xystore = useStoreApi<XYFlowTypes.Node, XYFlowTypes.Edge>()
|
|
106
|
-
const overlay = useOverlayDialog()
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
if (historySubjectId !== subject.id) {
|
|
110
|
-
historyOps.set(subject.id)
|
|
111
|
-
}
|
|
112
|
-
}, [subject.id])
|
|
138
|
+
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(true)
|
|
113
139
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
const containerviewport = useStore(selectContainerViewport)
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* WORKAROUND - Called on viewport change
|
|
144
|
+
* Viewport transform is not rounded to integers which results in blurry nodes on some resolution
|
|
145
|
+
* https://github.com/xyflow/xyflow/issues/3282
|
|
146
|
+
* https://github.com/likec4/likec4/issues/734
|
|
147
|
+
*/
|
|
148
|
+
useOnViewportChange({
|
|
149
|
+
onEnd: ({ x, y, zoom }) => {
|
|
150
|
+
const roundedX = Math.round(x),
|
|
151
|
+
roundedY = Math.round(y)
|
|
152
|
+
if (x !== roundedX || y !== roundedY) {
|
|
153
|
+
xystore.setState({ transform: [roundedX, roundedY, zoom] })
|
|
154
|
+
}
|
|
155
|
+
setZoomOnDoubleClick(zoom < 0.7)
|
|
119
156
|
}
|
|
120
|
-
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const initialFitviewFlagRef = useRef(false)
|
|
160
|
+
const viewBoundsRef = useSyncedRef(bounds)
|
|
121
161
|
|
|
122
162
|
const fitview = useDebouncedCallback(
|
|
123
163
|
() => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
const { width, height } = xystore.getState()
|
|
165
|
+
xyflow.setViewport(
|
|
166
|
+
getViewportForBounds(viewBoundsRef.current, width, height, minZoom, maxZoom, viewportPadding),
|
|
167
|
+
initialFitviewFlagRef.current ? { duration: 500 } : undefined
|
|
168
|
+
)
|
|
169
|
+
initialFitviewFlagRef.current = true
|
|
130
170
|
},
|
|
131
|
-
[xyflow],
|
|
132
|
-
|
|
171
|
+
[xyflow, minZoom, maxZoom, viewportPadding],
|
|
172
|
+
100
|
|
133
173
|
)
|
|
134
174
|
|
|
135
|
-
|
|
175
|
+
useUpdateEffect(() => {
|
|
176
|
+
fitview()
|
|
177
|
+
}, [containerviewport])
|
|
178
|
+
|
|
179
|
+
useDeepCompareEffect(() => {
|
|
136
180
|
const {
|
|
137
181
|
nodes: _nodes,
|
|
138
182
|
edges: _edges,
|
|
@@ -142,23 +186,24 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
142
186
|
height
|
|
143
187
|
} = xystore.getState()
|
|
144
188
|
|
|
145
|
-
const nextSubjectNode =
|
|
146
|
-
const currentSubjectNode =
|
|
189
|
+
const nextSubjectNode = findRootSubject(nodes)
|
|
190
|
+
const currentSubjectNode = findRootSubject(_nodes)
|
|
147
191
|
|
|
148
192
|
// If subject node is the same, don't animate
|
|
149
|
-
if (nextSubjectNode?.data.fqn === currentSubjectNode
|
|
150
|
-
setNodes(nodes)
|
|
193
|
+
if (currentSubjectNode && nextSubjectNode?.data.fqn === currentSubjectNode.data.fqn) {
|
|
194
|
+
setNodes(map(nodes, setPath(['data', 'initialAnimation'], false)))
|
|
151
195
|
setEdges(edges)
|
|
196
|
+
fitview()
|
|
152
197
|
return
|
|
153
198
|
}
|
|
154
199
|
|
|
155
200
|
if (!nextSubjectNode) {
|
|
156
201
|
console.error('Subject node not found')
|
|
157
|
-
} else if (nextSubjectNode.data.fqn !==
|
|
158
|
-
console.error(`Subject node mismatch, expected: ${
|
|
202
|
+
} else if (nextSubjectNode.data.fqn !== subjectId) {
|
|
203
|
+
console.error(`Subject node mismatch, expected: ${subjectId} got: ${nextSubjectNode.data.fqn}`)
|
|
159
204
|
}
|
|
160
205
|
|
|
161
|
-
const nextzoom = getViewportForBounds(bounds, width, height,
|
|
206
|
+
const nextzoom = getViewportForBounds(bounds, width, height, minZoom, maxZoom, viewportPadding).zoom
|
|
162
207
|
|
|
163
208
|
const nextSubjectCenter = nextSubjectNode && {
|
|
164
209
|
x: nextSubjectNode.position.x + (nextSubjectNode.width ?? 0) / 2,
|
|
@@ -166,17 +211,44 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
166
211
|
}
|
|
167
212
|
|
|
168
213
|
const existingNode = lastClickedNodeRef.current
|
|
169
|
-
?? xyflow.getNodes().find(n => n.type !== 'empty' && n.data.column !== '
|
|
214
|
+
?? xyflow.getNodes().find(n => n.type !== 'empty' && n.data.column !== 'subjects' && n.data.fqn === subjectId)
|
|
170
215
|
lastClickedNodeRef.current = null
|
|
171
216
|
// Animate from existing node to next subject node
|
|
172
|
-
if (nextSubjectCenter && existingNode) {
|
|
217
|
+
if (nextSubjectCenter && existingNode && currentSubjectNode) {
|
|
218
|
+
// Center of current subject
|
|
219
|
+
const currentSubjectInternalNode = xyflow.getInternalNode(currentSubjectNode.id)!
|
|
220
|
+
const currentSubjectCenter = centerXYInternalNode(currentSubjectInternalNode)
|
|
221
|
+
|
|
222
|
+
// Move to center of existing node
|
|
223
|
+
const existingInternalNode = xyflow.getInternalNode(existingNode.id)!
|
|
224
|
+
const existingDimensions = getNodeDimensions(existingInternalNode)
|
|
225
|
+
nextSubjectNode.data.layoutId = existingNode.id
|
|
226
|
+
|
|
173
227
|
// Dim all nodes except the existing node
|
|
174
228
|
setNodes(_nodes.map(n => {
|
|
229
|
+
if (n.id !== existingNode.id) {
|
|
230
|
+
return {
|
|
231
|
+
...n,
|
|
232
|
+
data: {
|
|
233
|
+
...n.data,
|
|
234
|
+
leaving: n.data.column === 'subjects',
|
|
235
|
+
dimmed: n.data.column === 'subjects' ? 'immediate' : false
|
|
236
|
+
}
|
|
237
|
+
// hidden: n.data.column === 'subjects'
|
|
238
|
+
} as XYFlowTypes.Node
|
|
239
|
+
}
|
|
240
|
+
// Move existing node
|
|
175
241
|
return {
|
|
176
|
-
...n,
|
|
242
|
+
...omit(n, ['parentId']),
|
|
243
|
+
position: {
|
|
244
|
+
x: currentSubjectCenter.x - existingDimensions.width / 2,
|
|
245
|
+
y: currentSubjectCenter.y - existingDimensions.height / 2
|
|
246
|
+
},
|
|
247
|
+
zIndex: ZIndexes.max,
|
|
177
248
|
data: {
|
|
178
249
|
...n.data,
|
|
179
|
-
|
|
250
|
+
leaving: false,
|
|
251
|
+
dimmed: false
|
|
180
252
|
}
|
|
181
253
|
} as XYFlowTypes.Node
|
|
182
254
|
}))
|
|
@@ -184,63 +256,65 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
184
256
|
...e,
|
|
185
257
|
data: {
|
|
186
258
|
...e.data,
|
|
187
|
-
dimmed:
|
|
188
|
-
}
|
|
259
|
+
dimmed: 'immediate' as const
|
|
260
|
+
},
|
|
261
|
+
hidden: e.source === existingNode.id
|
|
262
|
+
|| e.target === existingNode.id
|
|
263
|
+
|| isAncestor(existingNode.id, e.source)
|
|
264
|
+
|| isAncestor(existingNode.id, e.target)
|
|
189
265
|
})))
|
|
190
266
|
|
|
191
|
-
// Move to center of existing node
|
|
192
|
-
const existingInternalNode = xyflow.getInternalNode(existingNode.id)!
|
|
193
|
-
const existingDimensions = getNodeDimensions(existingInternalNode)
|
|
194
|
-
const existingCenter = {
|
|
195
|
-
x: existingInternalNode.internals.positionAbsolute.x + existingDimensions.width / 2,
|
|
196
|
-
y: existingInternalNode.internals.positionAbsolute.y + existingDimensions.height / 2
|
|
197
|
-
}
|
|
198
|
-
|
|
199
267
|
// Pick the smaller zoom level
|
|
200
268
|
const zoom = Math.min(
|
|
201
269
|
xyflow.getViewport().zoom,
|
|
202
|
-
nextzoom
|
|
203
|
-
nextSubjectNode.width! / existingDimensions.width,
|
|
204
|
-
nextSubjectNode.height! / existingDimensions.height
|
|
270
|
+
nextzoom
|
|
205
271
|
)
|
|
206
272
|
|
|
207
273
|
let isCancelled = false
|
|
208
274
|
|
|
209
|
-
|
|
210
|
-
//
|
|
275
|
+
const layout = async () => {
|
|
276
|
+
// allow frameer to render
|
|
277
|
+
await delay(150)
|
|
211
278
|
if (isCancelled) return
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
279
|
+
await xyflow.setCenter(currentSubjectCenter.x, currentSubjectCenter.y, { zoom, duration: 250 })
|
|
280
|
+
requestAnimationFrame(() => {
|
|
281
|
+
if (isCancelled) return
|
|
282
|
+
xyflow.setCenter(nextSubjectCenter.x, nextSubjectCenter.y, { zoom })
|
|
283
|
+
setNodes(nodes)
|
|
284
|
+
setEdges(edges)
|
|
285
|
+
fitview()
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
requestAnimationFrame(layout)
|
|
217
289
|
return () => {
|
|
218
290
|
isCancelled = true
|
|
219
291
|
}
|
|
220
292
|
}
|
|
221
|
-
setNodes(nodes)
|
|
293
|
+
setNodes(map(nodes, setPath(['data', 'initialAnimation'], false)))
|
|
222
294
|
setEdges(edges)
|
|
223
295
|
fitview()
|
|
224
296
|
return undefined
|
|
225
|
-
}, [nodes, edges])
|
|
297
|
+
}, [nodes, edges, subjectId])
|
|
226
298
|
|
|
227
299
|
return (
|
|
228
300
|
<ReactFlow
|
|
301
|
+
id={id}
|
|
229
302
|
defaultEdges={[] as XYFlowTypes.Edge[]}
|
|
230
303
|
defaultNodes={[] as XYFlowTypes.Node[]}
|
|
231
304
|
nodeTypes={nodeTypes}
|
|
232
305
|
edgeTypes={edgeTypes}
|
|
233
|
-
defaultMarkerColor="var(--
|
|
306
|
+
defaultMarkerColor="var(--likec4-relation-lineColor)"
|
|
307
|
+
className={cssReactflowMarker}
|
|
234
308
|
zoomOnPinch={true}
|
|
235
309
|
zoomOnScroll={false}
|
|
236
|
-
zoomOnDoubleClick={
|
|
237
|
-
maxZoom={
|
|
238
|
-
minZoom={
|
|
310
|
+
zoomOnDoubleClick={zoomOnDoubleClick}
|
|
311
|
+
maxZoom={maxZoom}
|
|
312
|
+
minZoom={minZoom}
|
|
239
313
|
fitView
|
|
240
314
|
fitViewOptions={{
|
|
241
|
-
padding:
|
|
242
|
-
maxZoom
|
|
243
|
-
minZoom
|
|
315
|
+
padding: viewportPadding,
|
|
316
|
+
maxZoom,
|
|
317
|
+
minZoom,
|
|
244
318
|
includeHiddenNodes: true
|
|
245
319
|
}}
|
|
246
320
|
preventScrolling={true}
|
|
@@ -248,7 +322,6 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
248
322
|
noPanClassName="nopan"
|
|
249
323
|
panOnScroll
|
|
250
324
|
panOnDrag
|
|
251
|
-
elementsSelectable
|
|
252
325
|
nodesFocusable={false}
|
|
253
326
|
edgesFocusable={false}
|
|
254
327
|
nodesDraggable={false}
|
|
@@ -285,136 +358,29 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
285
358
|
onEdgeMouseLeave={() => {
|
|
286
359
|
resetDimmedAndHovered(xyflow)
|
|
287
360
|
}}
|
|
288
|
-
onNodeClick={(e, node) => {
|
|
289
|
-
e.stopPropagation()
|
|
290
|
-
if (node.type === 'empty') return
|
|
291
|
-
lastClickedNodeRef.current = node
|
|
292
|
-
overlay.openOverlay({
|
|
293
|
-
relationshipsOf: node.data.fqn
|
|
294
|
-
})
|
|
295
|
-
}}
|
|
296
361
|
onNodeDoubleClick={(e) => {
|
|
297
362
|
e.stopPropagation()
|
|
298
363
|
}}
|
|
299
|
-
|
|
300
|
-
e
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
onEdgeClick={(e, edge) => {
|
|
304
|
-
e.stopPropagation()
|
|
305
|
-
// One relation in edge
|
|
306
|
-
const relationId = only(edge.data.relations)?.id
|
|
307
|
-
if (relationId) {
|
|
308
|
-
diagramStore.getState().onOpenSourceRelation?.(relationId)
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const nodeId = onlyOneUnique(edge.data, 'source') ? edge.source : edge.target
|
|
313
|
-
const next = xyflow.getNode(nodeId)
|
|
314
|
-
if (next && next.type !== 'empty') {
|
|
315
|
-
lastClickedNodeRef.current = next
|
|
316
|
-
overlay.openOverlay({
|
|
317
|
-
relationshipsOf: next.data.fqn
|
|
318
|
-
})
|
|
364
|
+
{...!zoomOnDoubleClick && {
|
|
365
|
+
onDoubleClick: e => {
|
|
366
|
+
e.stopPropagation()
|
|
367
|
+
fitview()
|
|
319
368
|
}
|
|
320
369
|
}}
|
|
370
|
+
{...rest}
|
|
321
371
|
>
|
|
322
|
-
<
|
|
323
|
-
|
|
324
|
-
<Button
|
|
325
|
-
leftSection={<IconArrowLeft stroke={3} size={14} />}
|
|
326
|
-
color="dimmed"
|
|
327
|
-
variant="subtle"
|
|
328
|
-
style={{
|
|
329
|
-
visibility: current > 0 ? 'visible' : 'hidden',
|
|
330
|
-
padding: '0.25rem 0.75rem'
|
|
331
|
-
}}
|
|
332
|
-
styles={{
|
|
333
|
-
label: {
|
|
334
|
-
fontWeight: 400
|
|
335
|
-
},
|
|
336
|
-
section: {
|
|
337
|
-
marginInlineEnd: 6
|
|
338
|
-
}
|
|
339
|
-
}}
|
|
340
|
-
size="sm"
|
|
341
|
-
onClick={(e) => {
|
|
342
|
-
e.stopPropagation()
|
|
343
|
-
historyOps.back()
|
|
344
|
-
}}>
|
|
345
|
-
Back
|
|
346
|
-
</Button>
|
|
347
|
-
<Space w={2} />
|
|
348
|
-
<Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
|
|
349
|
-
<Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap', userSelect: 'none' }}>Relationships of</Box>
|
|
350
|
-
<Box flex={'1 0 auto'}>
|
|
351
|
-
<SelectElement
|
|
352
|
-
scope={scope}
|
|
353
|
-
subject={subject.element}
|
|
354
|
-
onSelect={fqn =>
|
|
355
|
-
overlay.openOverlay({
|
|
356
|
-
relationshipsOf: fqn
|
|
357
|
-
})}
|
|
358
|
-
viewId={view.id} />
|
|
359
|
-
</Box>
|
|
360
|
-
<SegmentedControl
|
|
361
|
-
flex={'1 0 auto'}
|
|
362
|
-
size="xs"
|
|
363
|
-
withItemsBorders={false}
|
|
364
|
-
value={scope}
|
|
365
|
-
onChange={setScope as any}
|
|
366
|
-
data={[
|
|
367
|
-
{ label: 'Global', value: 'global' },
|
|
368
|
-
{ label: 'Current view', value: 'view', disabled: !viewIncludesSubject }
|
|
369
|
-
]}
|
|
370
|
-
/>
|
|
371
|
-
{showSubjectWarning && (
|
|
372
|
-
<Box
|
|
373
|
-
pos={'absolute'}
|
|
374
|
-
top={'calc(100% + .5rem)'}
|
|
375
|
-
left={'50%'}
|
|
376
|
-
w={'max-content'}
|
|
377
|
-
style={{
|
|
378
|
-
transform: 'translateX(-50%)',
|
|
379
|
-
textAlign: 'center',
|
|
380
|
-
cursor: 'pointer'
|
|
381
|
-
}}
|
|
382
|
-
onClick={(e) => {
|
|
383
|
-
e.stopPropagation()
|
|
384
|
-
setScope('global')
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
<Text fw={500} size="xs" c="orange" component="div">
|
|
388
|
-
Current view doesn't include this element, switched to Global
|
|
389
|
-
</Text>
|
|
390
|
-
</Box>
|
|
391
|
-
)}
|
|
392
|
-
</Group>
|
|
393
|
-
<Button
|
|
394
|
-
rightSection={<IconArrowRight stroke={3} size={14} />}
|
|
395
|
-
color="dimmed"
|
|
396
|
-
variant="subtle"
|
|
397
|
-
style={{
|
|
398
|
-
visibility: current + 1 < history.length ? 'visible' : 'hidden',
|
|
399
|
-
padding: '0.25rem 0.75rem'
|
|
400
|
-
}}
|
|
401
|
-
styles={{
|
|
402
|
-
label: {
|
|
403
|
-
fontWeight: 400
|
|
404
|
-
},
|
|
405
|
-
section: {
|
|
406
|
-
marginInlineStart: 4
|
|
407
|
-
}
|
|
408
|
-
}}
|
|
409
|
-
size="sm"
|
|
410
|
-
onClick={(e) => {
|
|
411
|
-
e.stopPropagation()
|
|
412
|
-
historyOps.forward()
|
|
413
|
-
}}>
|
|
414
|
-
Forward
|
|
415
|
-
</Button>
|
|
416
|
-
</Group>
|
|
417
|
-
</Panel>
|
|
372
|
+
{/* <Background variant={BackgroundVariant.Dots} size={2} gap={20} /> */}
|
|
373
|
+
{children}
|
|
418
374
|
</ReactFlow>
|
|
419
375
|
)
|
|
420
|
-
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function RelationshipsXYFlow(props: RelationshipsXYFlowProps) {
|
|
379
|
+
return (
|
|
380
|
+
<ReactFlowProvider
|
|
381
|
+
defaultNodes={[]}
|
|
382
|
+
defaultEdges={[]}>
|
|
383
|
+
<RelationshipsXYFlowWrapped {...props} />
|
|
384
|
+
</ReactFlowProvider>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
@@ -17,8 +17,8 @@ export namespace XYFlowTypes {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
type NodeProps = {
|
|
20
|
-
|
|
21
|
-
column: 'incomers' | '
|
|
20
|
+
depth?: number
|
|
21
|
+
column: 'incomers' | 'subjects' | 'outgoers'
|
|
22
22
|
fqn: Fqn
|
|
23
23
|
existsInCurrentView: boolean
|
|
24
24
|
element: Pick<ComputedNode, 'color' | 'title' | 'description' | 'shape' | 'kind'>
|
|
@@ -28,6 +28,12 @@ export namespace XYFlowTypes {
|
|
|
28
28
|
}
|
|
29
29
|
navigateTo: ViewID | null
|
|
30
30
|
hovered?: boolean
|
|
31
|
+
layoutId?: string
|
|
32
|
+
leaving?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
initialAnimation?: boolean
|
|
31
37
|
/**
|
|
32
38
|
* Whether the node is dimmed
|
|
33
39
|
* 'immediate' means that the node is dimmed without delay
|
|
@@ -43,9 +49,13 @@ export namespace XYFlowTypes {
|
|
|
43
49
|
|
|
44
50
|
export type EmptyNode = SetRequired<
|
|
45
51
|
ReactFlowNode<{
|
|
46
|
-
column: 'incomers' | 'outgoers'
|
|
52
|
+
column: 'incomers' | 'subjects' | 'outgoers'
|
|
47
53
|
hovered?: boolean
|
|
48
54
|
dimmed?: boolean
|
|
55
|
+
/**
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
initialAnimation?: boolean
|
|
49
59
|
}, 'empty'>,
|
|
50
60
|
'type'
|
|
51
61
|
>
|
|
@@ -55,8 +65,9 @@ export namespace XYFlowTypes {
|
|
|
55
65
|
export type Edge = Omit<ReactFlowEdge, 'data' | 'type'> & {
|
|
56
66
|
data: {
|
|
57
67
|
relations: [Relation, ...Relation[]]
|
|
68
|
+
includedInCurrentView: boolean
|
|
58
69
|
hovered?: boolean
|
|
59
|
-
dimmed?: boolean
|
|
70
|
+
dimmed?: 'immediate' | boolean
|
|
60
71
|
}
|
|
61
72
|
type: 'relation'
|
|
62
73
|
}
|