@likec4/diagram 1.14.0 → 1.15.1
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 +10 -10
- package/src/LikeC4CustomColors.tsx +6 -3
- package/src/LikeC4Diagram.tsx +6 -5
- package/src/overlays/Overlays.css.ts +7 -0
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +1 -1
- package/src/overlays/relationships-of/use-layouted-relationships.ts +14 -15
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +3 -0
- package/src/state/DiagramContext.tsx +3 -2
- package/src/state/diagram-to-xyflow.ts +31 -20
- package/src/state/diagramStore.ts +12 -7
- package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
- package/src/xyflow/types.ts +1 -0
- package/src/xyflow/useLayoutConstraints.ts +38 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likec4/diagram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bugs": "https://github.com/likec4/likec4/issues",
|
|
6
6
|
"homepage": "https://likec4.dev",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./src/index.ts"
|
|
25
25
|
},
|
|
26
|
-
"packageManager": "yarn@4.5.
|
|
26
|
+
"packageManager": "yarn@4.5.1",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
29
|
"lint": "run -T eslint src/ --fix",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@dagrejs/dagre": "^1.1.4",
|
|
34
|
-
"@likec4/core": "1.
|
|
35
|
-
"@mantine/vanilla-extract": "7.13.
|
|
34
|
+
"@likec4/core": "1.15.1",
|
|
35
|
+
"@mantine/vanilla-extract": "7.13.4",
|
|
36
36
|
"@react-hookz/web": "^24.0.4",
|
|
37
37
|
"@tabler/icons-react": "3.17.0",
|
|
38
38
|
"@vanilla-extract/css": "^1.16.0",
|
|
@@ -47,20 +47,20 @@
|
|
|
47
47
|
"fast-equals": "^5.0.1",
|
|
48
48
|
"framer-motion": "^11.11.9",
|
|
49
49
|
"khroma": "^2.1.0",
|
|
50
|
-
"remeda": "^2.
|
|
50
|
+
"remeda": "^2.16.0",
|
|
51
51
|
"type-fest": "4.26.1",
|
|
52
52
|
"zustand": "^4.5.5"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@mantine/core": "^7.13.
|
|
56
|
-
"@mantine/hooks": "^7.13.
|
|
55
|
+
"@mantine/core": "^7.13.4",
|
|
56
|
+
"@mantine/hooks": "^7.13.4",
|
|
57
57
|
"react": "^18.3.1",
|
|
58
58
|
"react-dom": "^18.3.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@likec4/tsconfig": "1.
|
|
62
|
-
"@mantine/core": "7.13.
|
|
63
|
-
"@mantine/hooks": "7.13.
|
|
61
|
+
"@likec4/tsconfig": "1.15.1",
|
|
62
|
+
"@mantine/core": "7.13.4",
|
|
63
|
+
"@mantine/hooks": "7.13.4",
|
|
64
64
|
"@types/bezier-js": "^4.1.3",
|
|
65
65
|
"@types/d3-shape": "^3.1.6",
|
|
66
66
|
"@types/react": "^18.3.11",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { type ThemeColorValues } from '@likec4/core'
|
|
2
2
|
import { useMantineStyleNonce } from '@mantine/core'
|
|
3
|
+
import { deepEqual } from 'fast-equals'
|
|
4
|
+
import { memo } from 'react'
|
|
3
5
|
import { entries } from 'remeda'
|
|
4
6
|
import type { LikeC4CustomColorsProperties } from './LikeC4CustomColors.props'
|
|
5
7
|
import { vars } from './theme-vars'
|
|
6
8
|
|
|
7
9
|
type CSSVarFunction = `var(--${string})` | `var(--${string}, ${string | number} )`
|
|
8
10
|
|
|
9
|
-
export
|
|
11
|
+
export const LikeC4CustomColors = memo<LikeC4CustomColorsProperties>(({ customColors }) => {
|
|
10
12
|
function toStyle(name: String, colorValues: ThemeColorValues): String {
|
|
11
13
|
const rules = new Array<String>(
|
|
12
14
|
...entries(colorValues.elements).map(([key, value]) => `${stripCssVarReference(vars.element[key])}: ${value};`),
|
|
@@ -34,7 +36,8 @@ export function LikeC4CustomColors({ customColors }: LikeC4CustomColorsPropertie
|
|
|
34
36
|
|
|
35
37
|
return (
|
|
36
38
|
<>
|
|
37
|
-
<style type="text/css" dangerouslySetInnerHTML={{ __html: styles }}
|
|
39
|
+
<style type="text/css" dangerouslySetInnerHTML={{ __html: styles }} nonce={nonce} />
|
|
38
40
|
</>
|
|
39
41
|
)
|
|
40
|
-
}
|
|
42
|
+
}, deepEqual)
|
|
43
|
+
LikeC4CustomColors.displayName = 'LikeC4CustomColors'
|
package/src/LikeC4Diagram.tsx
CHANGED
|
@@ -67,10 +67,10 @@ export function LikeC4Diagram({
|
|
|
67
67
|
initialWidth: initialWidth ?? view.bounds.width,
|
|
68
68
|
initialHeight: initialHeight ?? view.bounds.height
|
|
69
69
|
})
|
|
70
|
-
if (readonly !== true && !!where) {
|
|
71
|
-
console.warn('where filter is only supported in readonly mode')
|
|
72
|
-
}
|
|
73
70
|
useEffect(() => {
|
|
71
|
+
if (readonly !== true && where != null) {
|
|
72
|
+
console.warn('Ignore filter, supported in readonly mode only\n', { where })
|
|
73
|
+
}
|
|
74
74
|
if (hasLikec4model) {
|
|
75
75
|
return
|
|
76
76
|
}
|
|
@@ -80,7 +80,7 @@ export function LikeC4Diagram({
|
|
|
80
80
|
if (enableRelationshipsBrowser) {
|
|
81
81
|
console.warn('Invalid enableRelationshipsBrowser=true, requires LikeC4ModelProvider')
|
|
82
82
|
}
|
|
83
|
-
}
|
|
83
|
+
})
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
86
|
<EnsureMantine>
|
|
@@ -111,7 +111,8 @@ export function LikeC4Diagram({
|
|
|
111
111
|
enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
|
|
112
112
|
enableFocusMode={enableFocusMode}
|
|
113
113
|
enableRelationshipsBrowser={enableRelationshipsBrowser && hasLikec4model}
|
|
114
|
-
|
|
114
|
+
// Apply where filter only in readonly mode
|
|
115
|
+
whereFilter={readonly ? (where ?? null) : null}
|
|
115
116
|
renderIcon={renderIcon ?? null}
|
|
116
117
|
onCanvasClick={onCanvasClick ?? null}
|
|
117
118
|
onCanvasContextMenu={onCanvasContextMenu ?? null}
|
|
@@ -47,6 +47,13 @@ globalStyle(`:where(${container}) .react-flow__node-compound`, {
|
|
|
47
47
|
mixBlendMode: 'hard-light'
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
globalStyle(`${container} .react-flow__edges > svg`, {
|
|
51
|
+
mixBlendMode: 'plus-lighter'
|
|
52
|
+
})
|
|
53
|
+
globalStyle(`:where([data-mantine-color-scheme="light"]) ${container} .react-flow__edges > svg`, {
|
|
54
|
+
mixBlendMode: 'screen'
|
|
55
|
+
})
|
|
56
|
+
|
|
50
57
|
// const dialogFadeIn = keyframes({
|
|
51
58
|
// '0%': {
|
|
52
59
|
// opacity: 0,
|
|
@@ -346,7 +346,7 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
|
|
|
346
346
|
</Button>
|
|
347
347
|
<Space w={2} />
|
|
348
348
|
<Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
|
|
349
|
-
<Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap' }}>Relationships of</Box>
|
|
349
|
+
<Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap', userSelect: 'none' }}>Relationships of</Box>
|
|
350
350
|
<Box flex={'1 0 auto'}>
|
|
351
351
|
<SelectElement
|
|
352
352
|
scope={scope}
|
|
@@ -51,7 +51,7 @@ const Sizes = {
|
|
|
51
51
|
edgesep: 20
|
|
52
52
|
} satisfies GraphLabel,
|
|
53
53
|
edgeLabel: {
|
|
54
|
-
width:
|
|
54
|
+
width: 65
|
|
55
55
|
} satisfies Label,
|
|
56
56
|
|
|
57
57
|
emptyNodeOffset: 150,
|
|
@@ -63,7 +63,7 @@ const Sizes = {
|
|
|
63
63
|
// 0 means no spacer
|
|
64
64
|
spacerHeight: 0,
|
|
65
65
|
|
|
66
|
-
compoundLabelHeight:
|
|
66
|
+
compoundLabelHeight: 1
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export const ZIndexes = {
|
|
@@ -91,12 +91,11 @@ function createGraph() {
|
|
|
91
91
|
columns.reduce((prev, column) => {
|
|
92
92
|
const c = graphColumn(column)
|
|
93
93
|
g.setNode(c.id, {})
|
|
94
|
-
g.setNode(c.anchor, { width: 40, height:
|
|
94
|
+
g.setNode(c.anchor, { width: 40, height: 2 })
|
|
95
95
|
g.setParent(c.anchor, c.id)
|
|
96
96
|
if (prev) {
|
|
97
97
|
g.setEdge(prev, c.anchor, {
|
|
98
|
-
width: 0
|
|
99
|
-
weight: 10
|
|
98
|
+
width: 0
|
|
100
99
|
})
|
|
101
100
|
}
|
|
102
101
|
return c.anchor
|
|
@@ -258,9 +257,6 @@ function createNode(
|
|
|
258
257
|
|
|
259
258
|
g.setNode(k.id, sized())
|
|
260
259
|
if (xynode.type === 'compound') {
|
|
261
|
-
g.setNode(k.id, {
|
|
262
|
-
padding: 40
|
|
263
|
-
})
|
|
264
260
|
g.setNode(k.port, {
|
|
265
261
|
width: Sizes.nodeWidth - Sizes.dagre.ranksep,
|
|
266
262
|
height: Sizes.compoundLabelHeight
|
|
@@ -469,9 +465,10 @@ function layout(
|
|
|
469
465
|
|
|
470
466
|
forEachIncoming(sourceId, relations) {
|
|
471
467
|
const source = createNode('incomers', 'element', likec4model.element(sourceId), ctx)
|
|
472
|
-
g.setEdge(graphId(source).port, subject.id
|
|
473
|
-
|
|
474
|
-
|
|
468
|
+
g.setEdge(graphId(source).port, subject.id, {
|
|
469
|
+
width: Sizes.edgeLabel.width,
|
|
470
|
+
weight: source.type === 'compound' ? 0 : 1
|
|
471
|
+
})
|
|
475
472
|
|
|
476
473
|
subject.data.ports.left.push({
|
|
477
474
|
id: source.id,
|
|
@@ -490,9 +487,11 @@ function layout(
|
|
|
490
487
|
|
|
491
488
|
forEachOutgoing(targetId, relations) {
|
|
492
489
|
const target = createNode('outgoers', 'element', likec4model.element(targetId), ctx)
|
|
493
|
-
g.setEdge(subjectElement.id, graphId(target).port
|
|
490
|
+
g.setEdge(subjectElement.id, graphId(target).port, {
|
|
491
|
+
width: Sizes.edgeLabel.width,
|
|
492
|
+
weight: target.type === 'compound' ? 1 : 2
|
|
493
|
+
})
|
|
494
494
|
|
|
495
|
-
// const op = target.type === 'compound' ? 'unshift' : 'push'
|
|
496
495
|
subject.data.ports.right.push({
|
|
497
496
|
id: target.id,
|
|
498
497
|
type: 'out'
|
|
@@ -519,8 +518,8 @@ function layout(
|
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
const subjectPortsCount = Math.max(subject.data.ports.left.length, subject.data.ports.right.length)
|
|
522
|
-
if (subjectPortsCount >
|
|
523
|
-
g.node(subjectElement.id).height = Sizes.hodeHeight + (subjectPortsCount -
|
|
521
|
+
if (subjectPortsCount > 3) {
|
|
522
|
+
g.node(subjectElement.id).height = Sizes.hodeHeight + (subjectPortsCount - 3) * 5
|
|
524
523
|
}
|
|
525
524
|
|
|
526
525
|
const nodebounds = applyDagreLayout(ctx.g)
|
|
@@ -3,6 +3,7 @@ import { useMergedRef } from '@mantine/hooks'
|
|
|
3
3
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
4
4
|
import { useAnimate } from 'framer-motion'
|
|
5
5
|
import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
|
|
6
|
+
import { clone } from 'remeda'
|
|
6
7
|
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
7
8
|
import { useXYFlow, useXYStoreApi } from '../hooks/useXYFlow'
|
|
8
9
|
import { createDiagramStore, type DiagramInitialState, type DiagramZustandStore } from './diagramStore'
|
|
@@ -39,7 +40,7 @@ export function DiagramContextProvider({
|
|
|
39
40
|
xyflow,
|
|
40
41
|
view,
|
|
41
42
|
getContainer,
|
|
42
|
-
whereFilter:
|
|
43
|
+
whereFilter: clone(whereFilter),
|
|
43
44
|
...props
|
|
44
45
|
})
|
|
45
46
|
}
|
|
@@ -59,7 +60,7 @@ export function DiagramContextProvider({
|
|
|
59
60
|
const current = store.current
|
|
60
61
|
invariant(current, 'DiagramContext.store.current is not defined')
|
|
61
62
|
if (!deepEqual(whereFilter, current.getState().whereFilter)) {
|
|
62
|
-
current.setState({ whereFilter:
|
|
63
|
+
current.setState({ whereFilter: clone(whereFilter) }, false, 'update where filter')
|
|
63
64
|
}
|
|
64
65
|
current.getState().updateView(view)
|
|
65
66
|
},
|
|
@@ -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)
|
|
@@ -1065,7 +1065,12 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
1065
1065
|
const { nodeLookup, parentLookup } = xystore.getState()
|
|
1066
1066
|
|
|
1067
1067
|
const selectedNodes = new Set(nodeLookup.values().filter(n => n.selected).map(n => n.id))
|
|
1068
|
-
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
|
+
}
|
|
1069
1074
|
const constraints = createLayoutConstraints(xystore, nodesToAlign)
|
|
1070
1075
|
|
|
1071
1076
|
let getEdgePosition: (nodes: InternalNode<XYFlowNode>[]) => number
|
|
@@ -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
|
|
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"
|
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
|
})
|