@likec4/diagram 1.13.0 → 1.15.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 +9 -9
- package/src/LikeC4CustomColors.tsx +6 -3
- package/src/LikeC4Diagram.css.ts +4 -0
- package/src/LikeC4Diagram.tsx +9 -7
- package/src/overlays/Overlays.css.ts +124 -0
- package/src/overlays/Overlays.tsx +210 -0
- package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
- package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
- package/src/overlays/edge-details/SelectEdge.tsx +153 -0
- package/src/overlays/edge-details/_types.ts +38 -0
- package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
- package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
- package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
- package/src/overlays/index.tsx +1 -33
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
- package/src/overlays/relationships-of/_types.ts +13 -2
- package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
- package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
- package/src/state/DiagramContext.tsx +23 -5
- package/src/state/diagram-to-xyflow.ts +31 -20
- package/src/state/diagramStore.ts +30 -23
- package/src/xyflow/XYFlow.tsx +2 -1
- package/src/xyflow/XYFlowEvents.tsx +13 -1
- package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
- package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
- package/src/xyflow/types.ts +1 -0
- package/src/xyflow/useLayoutConstraints.ts +38 -19
- package/src/components/Overlay.css.ts +0 -78
- package/src/components/Overlay.tsx +0 -102
- package/src/components/index.ts +0 -2
- package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
- package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
- package/src/overlays/relationships-of/styles.css.ts +0 -34
- /package/src/{components → overlays}/OverlayContext.tsx +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DiagramEdge, DiagramNode, type DiagramView, ElementKind, type Fqn } from '@likec4/core'
|
|
2
2
|
import { nonNullable, whereOperatorAsPredicate } from '@likec4/core'
|
|
3
3
|
import { hasAtLeast } from 'remeda'
|
|
4
|
+
import type { UnionToIntersection } from 'type-fest'
|
|
4
5
|
import type { WhereOperator } from '../LikeC4Diagram.props'
|
|
5
6
|
import { ZIndexes } from '../xyflow/const'
|
|
6
7
|
import type { XYFlowEdge, XYFlowNode } from '../xyflow/types'
|
|
@@ -55,7 +56,7 @@ export function diagramViewToXYFlowData(
|
|
|
55
56
|
let next: typeof traverse[0] | undefined
|
|
56
57
|
while ((next = traverse.shift())) {
|
|
57
58
|
const { node, parent } = next
|
|
58
|
-
const isCompound = hasAtLeast(node.children, 1)
|
|
59
|
+
const isCompound = hasAtLeast(node.children, 1) || node.kind == ElementKind.Group
|
|
59
60
|
if (isCompound) {
|
|
60
61
|
traverse.push(...node.children.map(child => ({ node: nodeById(child), parent: node })))
|
|
61
62
|
}
|
|
@@ -69,35 +70,45 @@ export function diagramViewToXYFlowData(
|
|
|
69
70
|
position.y -= parent.position[1]
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
// const outEdges = node.outEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
|
|
73
|
-
// const inEdges = node.inEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
|
|
74
|
-
|
|
75
73
|
const id = ns + node.id
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
|
|
75
|
+
const base: Omit<UnionToIntersection<XYFlowNode>, 'data'> = {
|
|
78
76
|
id,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
fqn: node.id,
|
|
82
|
-
element: node
|
|
83
|
-
},
|
|
84
|
-
draggable,
|
|
85
|
-
selectable: opts.selectable,
|
|
77
|
+
draggable: opts.draggable,
|
|
78
|
+
selectable: opts.selectable && node.kind !== ElementKind.Group,
|
|
86
79
|
focusable: opts.selectable && !isCompound,
|
|
87
80
|
deletable: false,
|
|
88
81
|
position,
|
|
89
82
|
zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
|
|
90
83
|
width: node.width,
|
|
91
84
|
height: node.height,
|
|
92
|
-
hidden: !visiblePredicate(node),
|
|
93
|
-
// parentId: parent ? ns + parent.id : null,
|
|
85
|
+
hidden: node.kind !== ElementKind.Group && !visiblePredicate(node),
|
|
94
86
|
...(parent && {
|
|
95
87
|
parentId: ns + parent.id
|
|
96
|
-
}),
|
|
97
|
-
...(isCompound && {
|
|
98
|
-
dragHandle: '.likec4-compound-title'
|
|
99
88
|
})
|
|
100
|
-
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
xynodes.push(
|
|
92
|
+
isCompound
|
|
93
|
+
? {
|
|
94
|
+
...base,
|
|
95
|
+
type: 'compound',
|
|
96
|
+
data: {
|
|
97
|
+
fqn: node.id,
|
|
98
|
+
isViewGroup: node.kind === ElementKind.Group,
|
|
99
|
+
element: node
|
|
100
|
+
},
|
|
101
|
+
dragHandle: '.likec4-compound-title'
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
...base,
|
|
105
|
+
type: 'element',
|
|
106
|
+
data: {
|
|
107
|
+
fqn: node.id,
|
|
108
|
+
element: node
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
for (const edge of view.edges) {
|
|
@@ -395,7 +395,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
395
395
|
selectable: nodesSelectable
|
|
396
396
|
})
|
|
397
397
|
|
|
398
|
-
update.xynodes = update.xynodes.map(update => {
|
|
398
|
+
update.xynodes = update.xynodes.map((update) => {
|
|
399
399
|
const existing = xynodes.find(n => n.id === update.id)
|
|
400
400
|
if (
|
|
401
401
|
existing
|
|
@@ -403,18 +403,18 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
403
403
|
&& eq(existing.parentId ?? null, update.parentId ?? null)
|
|
404
404
|
) {
|
|
405
405
|
if (
|
|
406
|
-
existing.
|
|
407
|
-
&& existing.width === update.width
|
|
406
|
+
existing.width === update.width
|
|
408
407
|
&& existing.height === update.height
|
|
408
|
+
&& eq(existing.hidden ?? false, update.hidden ?? false)
|
|
409
409
|
&& eq(existing.position, update.position)
|
|
410
|
-
&& eq(existing.data
|
|
410
|
+
&& eq(existing.data, update.data)
|
|
411
411
|
) {
|
|
412
412
|
return existing
|
|
413
413
|
}
|
|
414
414
|
return {
|
|
415
415
|
...existing,
|
|
416
416
|
...update
|
|
417
|
-
}
|
|
417
|
+
} as XYFlowNode
|
|
418
418
|
}
|
|
419
419
|
return update
|
|
420
420
|
})
|
|
@@ -425,7 +425,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
425
425
|
const existing = xyedges.find(n => n.id === update.id)
|
|
426
426
|
if (existing) {
|
|
427
427
|
if (
|
|
428
|
-
existing.hidden
|
|
428
|
+
eq(existing.hidden ?? false, update.hidden ?? false)
|
|
429
429
|
&& eq(existing.data.label, update.data.label)
|
|
430
430
|
&& eq(existing.data.controlPoints, update.data.controlPoints)
|
|
431
431
|
&& eq(existing.data.edge, update.data.edge)
|
|
@@ -850,22 +850,21 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
850
850
|
},
|
|
851
851
|
|
|
852
852
|
openOverlay: (overlay) => {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
noReplace,
|
|
858
|
-
'openOverlay'
|
|
859
|
-
)
|
|
853
|
+
if (eq(overlay, get().activeOverlay)) {
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
set({ activeOverlay: overlay }, noReplace, 'openOverlay')
|
|
860
857
|
},
|
|
861
858
|
closeOverlay: () => {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
859
|
+
if (get().activeOverlay !== null) {
|
|
860
|
+
set(
|
|
861
|
+
{
|
|
862
|
+
activeOverlay: null
|
|
863
|
+
},
|
|
864
|
+
noReplace,
|
|
865
|
+
'openOverlay'
|
|
866
|
+
)
|
|
867
|
+
}
|
|
869
868
|
},
|
|
870
869
|
|
|
871
870
|
fitDiagram: (duration = 500) => {
|
|
@@ -875,7 +874,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
875
874
|
const bounds = view.bounds
|
|
876
875
|
const maxZoom = Math.max(1, transform[2])
|
|
877
876
|
const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
|
|
878
|
-
panZoom?.setViewport(viewport, { duration })
|
|
877
|
+
panZoom?.setViewport(viewport, duration > 0 ? { duration } : undefined)
|
|
879
878
|
},
|
|
880
879
|
|
|
881
880
|
nextDynamicStep: (increment = 1) => {
|
|
@@ -969,8 +968,11 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
969
968
|
},
|
|
970
969
|
|
|
971
970
|
onInit: (instance) => {
|
|
972
|
-
const { xyflow, initialized } = get()
|
|
971
|
+
const { xyflow, initialized, fitViewEnabled, fitDiagram } = get()
|
|
973
972
|
if (!initialized || xyflow !== instance) {
|
|
973
|
+
if (fitViewEnabled) {
|
|
974
|
+
fitDiagram(0)
|
|
975
|
+
}
|
|
974
976
|
set(
|
|
975
977
|
{
|
|
976
978
|
xyflow: instance,
|
|
@@ -1063,7 +1065,12 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
1063
1065
|
const { nodeLookup, parentLookup } = xystore.getState()
|
|
1064
1066
|
|
|
1065
1067
|
const selectedNodes = new Set(nodeLookup.values().filter(n => n.selected).map(n => n.id))
|
|
1066
|
-
const nodesToAlign =
|
|
1068
|
+
const nodesToAlign = [...selectedNodes.difference(new Set(parentLookup.keys()))]
|
|
1069
|
+
|
|
1070
|
+
if (!hasAtLeast(nodesToAlign, 2)) {
|
|
1071
|
+
console.warn('At least 2 nodes must be selected to align')
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1067
1074
|
const constraints = createLayoutConstraints(xystore, nodesToAlign)
|
|
1068
1075
|
|
|
1069
1076
|
let getEdgePosition: (nodes: InternalNode<XYFlowNode>[]) => number
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -128,7 +128,8 @@ export function XYFlow({
|
|
|
128
128
|
zoomOnDoubleClick={false}
|
|
129
129
|
maxZoom={zoomable ? MaxZoom : 1}
|
|
130
130
|
minZoom={zoomable ? MinZoom : 1}
|
|
131
|
-
|
|
131
|
+
// Fitview is handled by store in onInit
|
|
132
|
+
fitView={false}
|
|
132
133
|
fitViewOptions={useMemo(() => ({
|
|
133
134
|
minZoom: MinZoom,
|
|
134
135
|
maxZoom: 1,
|
|
@@ -225,11 +225,13 @@ export function useXYFlowEvents() {
|
|
|
225
225
|
}
|
|
226
226
|
if (!onEdgeClick) {
|
|
227
227
|
event.stopPropagation()
|
|
228
|
+
return
|
|
228
229
|
}
|
|
229
230
|
} else if (nodesSelectable) {
|
|
230
231
|
xystore.getState().addSelectedEdges([xyedge.id])
|
|
231
232
|
if (!onEdgeClick) {
|
|
232
233
|
event.stopPropagation()
|
|
234
|
+
return
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
onEdgeClick?.(
|
|
@@ -245,7 +247,9 @@ export function useXYFlowEvents() {
|
|
|
245
247
|
enableDynamicViewWalkthrough,
|
|
246
248
|
focusOnNode,
|
|
247
249
|
activeWalkthrough,
|
|
248
|
-
activateWalkthrough
|
|
250
|
+
activateWalkthrough,
|
|
251
|
+
openOverlay,
|
|
252
|
+
showRelationshipDetails
|
|
249
253
|
} = diagramApi.getState()
|
|
250
254
|
// if we are in dynamic view, and clicked on an edge, activate the step
|
|
251
255
|
if (isDynamicView && enableDynamicViewWalkthrough) {
|
|
@@ -257,6 +261,14 @@ export function useXYFlowEvents() {
|
|
|
257
261
|
return
|
|
258
262
|
}
|
|
259
263
|
|
|
264
|
+
if (showRelationshipDetails) {
|
|
265
|
+
openOverlay({
|
|
266
|
+
edgeDetails: xyedge.data.edge.id
|
|
267
|
+
})
|
|
268
|
+
event.stopPropagation()
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
260
272
|
if (enableFocusMode) {
|
|
261
273
|
focusOnNode(xyedge.source)
|
|
262
274
|
event.stopPropagation()
|
|
@@ -137,7 +137,6 @@ const curve = d3line<XYPosition>()
|
|
|
137
137
|
export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
|
|
138
138
|
id,
|
|
139
139
|
data,
|
|
140
|
-
selected,
|
|
141
140
|
sourceX,
|
|
142
141
|
sourceY,
|
|
143
142
|
targetX,
|
|
@@ -438,10 +437,17 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
|
|
|
438
437
|
const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
|
|
439
438
|
const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
|
|
440
439
|
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
const edgeZIndex = edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge
|
|
441
|
+
let labelZIndex
|
|
442
|
+
if (isEdgePathEditable) {
|
|
443
443
|
// Move label below ControlPoints, otherwise they don't capture events
|
|
444
|
-
labelZIndex =
|
|
444
|
+
labelZIndex = edgeZIndex - 1
|
|
445
|
+
}
|
|
446
|
+
else if (isHovered) {
|
|
447
|
+
// Move above the elements
|
|
448
|
+
labelZIndex = ZIndexes.Element + 1
|
|
449
|
+
} else {
|
|
450
|
+
labelZIndex = edgeZIndex + 1
|
|
445
451
|
}
|
|
446
452
|
|
|
447
453
|
return (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DiagramNode, LikeC4Model, nameFromFqn } from '@likec4/core'
|
|
1
|
+
import { type DiagramNode, invariant, LikeC4Model, nameFromFqn } from '@likec4/core'
|
|
2
2
|
import {
|
|
3
3
|
ActionIcon,
|
|
4
4
|
Box,
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
Tooltip as MantineTooltip,
|
|
17
17
|
TooltipGroup
|
|
18
18
|
} from '@mantine/core'
|
|
19
|
-
import { IconArrowRight, IconFileSymlink, IconZoomScan } from '@tabler/icons-react'
|
|
19
|
+
import { IconArrowRight, IconArrowsMaximize, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@tabler/icons-react'
|
|
20
20
|
import clsx from 'clsx'
|
|
21
|
-
import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren } from 'react'
|
|
21
|
+
import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren, useCallback } from 'react'
|
|
22
22
|
import { filter, isTruthy, map, partition, pipe } from 'remeda'
|
|
23
23
|
import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useXYNodesData } from '../../hooks'
|
|
24
24
|
import type { RelationshipData } from '../types'
|
|
@@ -47,10 +47,12 @@ export function RelationshipsDropdownMenu({
|
|
|
47
47
|
disabled?: boolean | undefined
|
|
48
48
|
likec4model: LikeC4Model
|
|
49
49
|
}>) {
|
|
50
|
+
const openOverlay = useDiagramState(s => s.openOverlay)
|
|
50
51
|
const portalProps = useMantinePortalProps()
|
|
51
52
|
const [sourceXYNode, targetXYNode] = useXYNodesData([edge.source, edge.target])
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
invariant(sourceXYNode, `Source XYNode ${edge.source} not found for edge ${edge.id}`)
|
|
55
|
+
invariant(targetXYNode, `Target XYNode ${edge.target} not found for edge ${edge.id}`)
|
|
54
56
|
|
|
55
57
|
const [direct, nested] = pipe(
|
|
56
58
|
edge.relations,
|
|
@@ -70,14 +72,11 @@ export function RelationshipsDropdownMenu({
|
|
|
70
72
|
partition(r => r.relationship.source === edge.source && r.relationship.target === edge.target)
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
if (direct.length + nested.length === 0) {
|
|
74
|
-
return <>{children}</>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
75
|
const renderRelationship = (relationship: LikeC4Model.ViewModel.Relationship, index: number) => (
|
|
78
76
|
<Fragment key={relationship.id}>
|
|
79
77
|
{index > 0 && <MenuDivider opacity={0.65} />}
|
|
80
78
|
<MenuItem
|
|
79
|
+
onClick={onClickOpenOverlay}
|
|
81
80
|
component={Relationship}
|
|
82
81
|
relationship={relationship}
|
|
83
82
|
sourceNode={sourceXYNode.data.element}
|
|
@@ -86,6 +85,17 @@ export function RelationshipsDropdownMenu({
|
|
|
86
85
|
</Fragment>
|
|
87
86
|
)
|
|
88
87
|
|
|
88
|
+
const onClickOpenOverlay = useCallback((e: React.MouseEvent): void => {
|
|
89
|
+
e.stopPropagation()
|
|
90
|
+
openOverlay({
|
|
91
|
+
edgeDetails: edge.id
|
|
92
|
+
})
|
|
93
|
+
}, [edge.id, openOverlay])
|
|
94
|
+
|
|
95
|
+
if (direct.length + nested.length === 0) {
|
|
96
|
+
return <>{children}</>
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
return (
|
|
90
100
|
<Menu
|
|
91
101
|
trigger={'click-hover'}
|
|
@@ -107,7 +117,6 @@ export function RelationshipsDropdownMenu({
|
|
|
107
117
|
onPointerDownCapture={stopPropagation}
|
|
108
118
|
onPointerDown={stopPropagation}
|
|
109
119
|
onClick={stopPropagation}
|
|
110
|
-
onDoubleClick={stopPropagation}
|
|
111
120
|
>
|
|
112
121
|
{direct.length > 0 && (
|
|
113
122
|
<>
|
|
@@ -122,22 +131,15 @@ export function RelationshipsDropdownMenu({
|
|
|
122
131
|
{nested.map(renderRelationship)}
|
|
123
132
|
</>
|
|
124
133
|
)}
|
|
125
|
-
{
|
|
126
|
-
/* <Box pos={'absolute'} top={5} right={6}>
|
|
134
|
+
<Box pos={'absolute'} top={5} right={6}>
|
|
127
135
|
<ActionIcon
|
|
128
136
|
size={24}
|
|
129
|
-
variant="
|
|
130
|
-
onClick={
|
|
131
|
-
e.stopPropagation()
|
|
132
|
-
diagramApi.getState().openOverlay({
|
|
133
|
-
edgeDetails: edge.id
|
|
134
|
-
})
|
|
135
|
-
}}
|
|
137
|
+
variant="subtle"
|
|
138
|
+
onClick={onClickOpenOverlay}
|
|
136
139
|
>
|
|
137
|
-
<
|
|
140
|
+
<IconInfoCircle style={{ width: '70%' }} />
|
|
138
141
|
</ActionIcon>
|
|
139
|
-
</Box>
|
|
140
|
-
}
|
|
142
|
+
</Box>
|
|
141
143
|
</MenuDropdown>
|
|
142
144
|
</Menu>
|
|
143
145
|
)
|
|
@@ -195,6 +197,7 @@ const Relationship = forwardRef<
|
|
|
195
197
|
variant="default"
|
|
196
198
|
onPointerDownCapture={stopPropagation}
|
|
197
199
|
onClick={event => {
|
|
200
|
+
event.stopPropagation()
|
|
198
201
|
diagramApi.getState().onNavigateTo?.(navigateTo, event)
|
|
199
202
|
}}
|
|
200
203
|
role="button"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ThemeColor } from '@likec4/core'
|
|
2
2
|
import { ActionIcon, Box, Text, Tooltip } from '@mantine/core'
|
|
3
3
|
import { useDebouncedValue } from '@mantine/hooks'
|
|
4
4
|
import { IconTransform, IconZoomScan } from '@tabler/icons-react'
|
|
@@ -127,14 +127,12 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
127
127
|
selected = false,
|
|
128
128
|
dragging = false,
|
|
129
129
|
data: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
style,
|
|
133
|
-
depth = 1,
|
|
134
|
-
...element
|
|
135
|
-
}
|
|
130
|
+
isViewGroup,
|
|
131
|
+
element
|
|
136
132
|
}
|
|
137
133
|
}) {
|
|
134
|
+
const { depth, style, color } = element
|
|
135
|
+
const isNotViewGroup = !isViewGroup
|
|
138
136
|
const opacity = clamp((style.opacity ?? 100) / 100, {
|
|
139
137
|
min: 0,
|
|
140
138
|
max: 1
|
|
@@ -164,9 +162,11 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
164
162
|
hasOnNavigateTo: !!s.onNavigateTo,
|
|
165
163
|
enableRelationshipsMode: s.enableRelationshipsBrowser
|
|
166
164
|
}))
|
|
167
|
-
|
|
165
|
+
// If this is a view group, we don't want to show the navigate button
|
|
166
|
+
const isNavigable = isNotViewGroup && !!element.navigateTo && hasOnNavigateTo
|
|
167
|
+
const hasRelationshipsBrowser = isNotViewGroup && enableRelationshipsMode
|
|
168
168
|
|
|
169
|
-
const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
|
|
169
|
+
const _isToolbarVisible = isNotViewGroup && isEditable && (isHovered || (import.meta.env.DEV && selected))
|
|
170
170
|
const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
|
|
171
171
|
|
|
172
172
|
const [animateVariants, animateHandlers] = useFramerAnimateVariants()
|
|
@@ -263,7 +263,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
263
263
|
component="div"
|
|
264
264
|
className={clsx(
|
|
265
265
|
css.title,
|
|
266
|
-
(isNavigable ||
|
|
266
|
+
(isNavigable || hasRelationshipsBrowser) && css.titleWithNavigation,
|
|
267
267
|
'likec4-compound-title'
|
|
268
268
|
)}>
|
|
269
269
|
{element.title}
|
|
@@ -286,7 +286,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
286
286
|
<IconZoomScan stroke={1.8} style={{ width: '75%' }} />
|
|
287
287
|
</ActionIcon>
|
|
288
288
|
)}
|
|
289
|
-
{
|
|
289
|
+
{hasRelationshipsBrowser && (
|
|
290
290
|
<Tooltip
|
|
291
291
|
fz="xs"
|
|
292
292
|
color="dark"
|
|
@@ -260,7 +260,7 @@ function GoToSourceButton({ elementId }: { elementId: Fqn }) {
|
|
|
260
260
|
e.stopPropagation()
|
|
261
261
|
diagramApi.getState().onOpenSourceElement?.(elementId)
|
|
262
262
|
}}>
|
|
263
|
-
<IconFileSymlink
|
|
263
|
+
<IconFileSymlink stroke={1.8} style={{ width: '70%' }} />
|
|
264
264
|
</ActionIcon>
|
|
265
265
|
</Tooltip>
|
|
266
266
|
)
|
package/src/xyflow/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { invariant, isAncestor, nonNullable } from '@likec4/core'
|
|
1
|
+
import { invariant, isAncestor, type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
|
|
2
2
|
import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
3
3
|
import { getNodeDimensions } from '@xyflow/system'
|
|
4
4
|
import { useMemo, useRef } from 'react'
|
|
5
|
-
import { isNullish } from 'remeda'
|
|
5
|
+
import { filter, hasAtLeast, isNullish, map, pipe } from 'remeda'
|
|
6
6
|
import { useDiagramStoreApi } from '../hooks'
|
|
7
7
|
import { type XYStoreApi } from '../hooks/useXYFlow'
|
|
8
8
|
import type { XYFlowNode } from './types'
|
|
@@ -99,11 +99,27 @@ type NodePositionUpdater = (
|
|
|
99
99
|
|
|
100
100
|
export function createLayoutConstraints(
|
|
101
101
|
xyflowApi: XYStoreApi,
|
|
102
|
-
editingNodeIds:
|
|
102
|
+
editingNodeIds: NonEmptyArray<string>
|
|
103
103
|
) {
|
|
104
104
|
const { parentLookup, nodeLookup } = xyflowApi.getState()
|
|
105
105
|
const rects = new Map<string, Leaf | Compound>()
|
|
106
106
|
|
|
107
|
+
const ancestorsOf = (nodeId: string) => {
|
|
108
|
+
const ancestors = [] as string[]
|
|
109
|
+
const xynode = nodeLookup.get(nodeId)
|
|
110
|
+
let parent = xynode?.parentId
|
|
111
|
+
let parentNode: InternalNode<XYFlowNode> | undefined
|
|
112
|
+
while (parent && (parentNode = nodeLookup.get(parent))) {
|
|
113
|
+
ancestors.push(parentNode.id as NodeId)
|
|
114
|
+
parent = parentNode.parentId
|
|
115
|
+
}
|
|
116
|
+
return ancestors
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ancestorsOfDraggingNodes = new Set(
|
|
120
|
+
editingNodeIds.flatMap(ancestorsOf)
|
|
121
|
+
)
|
|
122
|
+
|
|
107
123
|
const traverse = new Array<{ xynode: InternalNode<XYFlowNode>; parent: Compound | null }>()
|
|
108
124
|
|
|
109
125
|
for (const [, xynode] of nodeLookup) {
|
|
@@ -117,11 +133,11 @@ export function createLayoutConstraints(
|
|
|
117
133
|
|
|
118
134
|
while (traverse.length > 0) {
|
|
119
135
|
const { xynode, parent } = traverse.shift()!
|
|
120
|
-
const isEditing = editingNodeIds.
|
|
136
|
+
const isEditing = editingNodeIds.includes(xynode.id)
|
|
121
137
|
|
|
122
138
|
// Traverse children if the node is a compound, not dragging, and is an ancestor of the dragging node
|
|
123
139
|
const shouldTraverse = !isEditing && xynode.type === 'compound'
|
|
124
|
-
&&
|
|
140
|
+
&& ancestorsOfDraggingNodes.has(xynode.id)
|
|
125
141
|
|
|
126
142
|
const rect = shouldTraverse ? new Compound(xynode, parent) : new Leaf(xynode, parent)
|
|
127
143
|
rects.set(xynode.id, rect)
|
|
@@ -157,7 +173,6 @@ export function createLayoutConstraints(
|
|
|
157
173
|
maxY: -Infinity
|
|
158
174
|
})
|
|
159
175
|
|
|
160
|
-
|
|
161
176
|
r.minX = childrenBB.minX - Rect.LeftPadding
|
|
162
177
|
r.minY = childrenBB.minY - Rect.TopPadding
|
|
163
178
|
r.maxX = childrenBB.maxX + Rect.RightPadding
|
|
@@ -200,11 +215,14 @@ export function createLayoutConstraints(
|
|
|
200
215
|
animationFrameId ??= requestAnimationFrame(() => {
|
|
201
216
|
animationFrameId = null
|
|
202
217
|
updater(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
pipe(
|
|
219
|
+
editingNodeIds,
|
|
220
|
+
filter(id => rects.has(id) && nodeLookup.has(id)),
|
|
221
|
+
map(id => ({
|
|
222
|
+
rect: nonNullable(rects.get(id)),
|
|
223
|
+
node: nonNullable(nodeLookup.get(id))
|
|
224
|
+
}))
|
|
225
|
+
)
|
|
208
226
|
)
|
|
209
227
|
updateXYFlowNodes()
|
|
210
228
|
})
|
|
@@ -229,17 +247,18 @@ export function useLayoutConstraints(): LayoutConstraints {
|
|
|
229
247
|
const { nodeLookup } = xystore.getState()
|
|
230
248
|
cancelSaveManualLayout()
|
|
231
249
|
|
|
232
|
-
const draggingNodes =
|
|
233
|
-
nodeLookup.values()
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
250
|
+
const draggingNodes = pipe(
|
|
251
|
+
Array.from(nodeLookup.values()),
|
|
252
|
+
filter(n => n.dragging === true || n.id === xynode.id || n.selected === true),
|
|
253
|
+
filter(n => n.draggable !== false),
|
|
254
|
+
map(x => x.id)
|
|
237
255
|
)
|
|
238
|
-
|
|
256
|
+
if (hasAtLeast(draggingNodes, 1)) {
|
|
257
|
+
solverRef.current = createLayoutConstraints(xystore, draggingNodes)
|
|
258
|
+
}
|
|
239
259
|
},
|
|
240
260
|
onNodeDrag: () => {
|
|
241
|
-
|
|
242
|
-
solverRef.current.onMove((nodes) => {
|
|
261
|
+
solverRef.current?.onMove((nodes) => {
|
|
243
262
|
nodes.forEach(({ rect, node }) => {
|
|
244
263
|
rect.positionAbsolute = node.internals.positionAbsolute
|
|
245
264
|
})
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { createVar, keyframes, style } from '@vanilla-extract/css'
|
|
2
|
-
import { whereDark } from '../theme-vars'
|
|
3
|
-
|
|
4
|
-
const dialogFadeIn = keyframes({
|
|
5
|
-
'0%': {
|
|
6
|
-
opacity: 0,
|
|
7
|
-
display: 'none'
|
|
8
|
-
},
|
|
9
|
-
'100%': {
|
|
10
|
-
opacity: 1,
|
|
11
|
-
display: 'block'
|
|
12
|
-
}
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const dialogFadeOut = keyframes({
|
|
16
|
-
'0%': {
|
|
17
|
-
opacity: 1,
|
|
18
|
-
transform: 'scale(1)',
|
|
19
|
-
display: 'block'
|
|
20
|
-
},
|
|
21
|
-
'100%': {
|
|
22
|
-
opacity: 0,
|
|
23
|
-
transform: 'scale(0.98)',
|
|
24
|
-
display: 'none'
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const backdropRgb = createVar('backdrop-rgb')
|
|
29
|
-
|
|
30
|
-
const dialogBackdropFadeIn = keyframes({
|
|
31
|
-
'0%': {
|
|
32
|
-
WebkitBackdropFilter: 'blur(1px)',
|
|
33
|
-
backdropFilter: 'blur(1px)',
|
|
34
|
-
backgroundColor: `rgb(${backdropRgb} / 30%)`
|
|
35
|
-
},
|
|
36
|
-
'100%': {
|
|
37
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
38
|
-
backdropFilter: 'blur(8px)',
|
|
39
|
-
backgroundColor: `rgb(${backdropRgb} / 85%)`
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
export const dialog = style({
|
|
44
|
-
top: 0,
|
|
45
|
-
left: 0,
|
|
46
|
-
padding: 0,
|
|
47
|
-
margin: 0,
|
|
48
|
-
boxSizing: 'border-box',
|
|
49
|
-
border: '0 solid transparent',
|
|
50
|
-
width: '100%',
|
|
51
|
-
maxWidth: '100dvw',
|
|
52
|
-
height: '100%',
|
|
53
|
-
maxHeight: '100dvh',
|
|
54
|
-
background: 'transparent',
|
|
55
|
-
animation: `${dialogFadeOut} 130ms cubic-bezier(0.50, 0, 1, 1)`,
|
|
56
|
-
transformOrigin: '50% 20%',
|
|
57
|
-
vars: {
|
|
58
|
-
[backdropRgb]: '255 255 255'
|
|
59
|
-
},
|
|
60
|
-
selectors: {
|
|
61
|
-
[`${whereDark} &`]: {
|
|
62
|
-
vars: {
|
|
63
|
-
[backdropRgb]: '36 36 36'
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
'&[open]': {
|
|
67
|
-
animation: `${dialogFadeIn} 200ms cubic-bezier(0, 0, 0.40, 1) forwards`
|
|
68
|
-
},
|
|
69
|
-
'&::backdrop': {
|
|
70
|
-
WebkitBackdropFilter: 'blur(1px)',
|
|
71
|
-
backdropFilter: 'blur(1px)',
|
|
72
|
-
backgroundColor: `rgb(${backdropRgb} / 30%)`
|
|
73
|
-
},
|
|
74
|
-
'&[open]::backdrop': {
|
|
75
|
-
animation: `${dialogBackdropFadeIn} 350ms cubic-bezier(0, 0, 0.40, 1) forwards`
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|