@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
+
import { mantine, vars, whereLight, xyvars } from '../../../theme-vars'
|
|
3
|
+
|
|
4
|
+
export const elementNode = style({
|
|
5
|
+
width: '100%',
|
|
6
|
+
height: '100%'
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const elementNodeContent = style({
|
|
10
|
+
width: '100%',
|
|
11
|
+
height: '100%',
|
|
12
|
+
display: 'flex',
|
|
13
|
+
flexDirection: 'column',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
gap: 6,
|
|
18
|
+
padding: 16
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const elementNodeTitle = style({
|
|
22
|
+
flex: '0 0 auto',
|
|
23
|
+
width: 'fit-content',
|
|
24
|
+
textAlign: 'center',
|
|
25
|
+
fontFamily: vars.element.font,
|
|
26
|
+
fontOpticalSizing: 'auto',
|
|
27
|
+
fontStyle: 'normal',
|
|
28
|
+
fontWeight: 500,
|
|
29
|
+
fontSize: 17,
|
|
30
|
+
lineHeight: 1.25,
|
|
31
|
+
textWrap: 'balance',
|
|
32
|
+
color: vars.element.hiContrast
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const elementNodeDescription = style({
|
|
36
|
+
flex: '0 0 auto',
|
|
37
|
+
width: 'fit-content',
|
|
38
|
+
textAlign: 'center',
|
|
39
|
+
fontFamily: vars.element.font,
|
|
40
|
+
fontOpticalSizing: 'auto',
|
|
41
|
+
fontStyle: 'normal',
|
|
42
|
+
fontSize: mantine.fontSizes.xs,
|
|
43
|
+
lineHeight: 1.25,
|
|
44
|
+
textWrap: 'balance',
|
|
45
|
+
color: vars.element.loContrast
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const compoundNodeBody = style({
|
|
49
|
+
width: '100%',
|
|
50
|
+
height: '100%',
|
|
51
|
+
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
|
|
52
|
+
background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
|
|
53
|
+
borderRadius: 4
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const compoundNodeTitle = style({
|
|
57
|
+
fontFamily: vars.compound.font,
|
|
58
|
+
fontOpticalSizing: 'auto',
|
|
59
|
+
fontStyle: 'normal',
|
|
60
|
+
fontWeight: 600,
|
|
61
|
+
fontSize: 14,
|
|
62
|
+
lineHeight: 1,
|
|
63
|
+
textTransform: 'uppercase',
|
|
64
|
+
paddingTop: 12,
|
|
65
|
+
paddingLeft: 10,
|
|
66
|
+
mixBlendMode: 'screen',
|
|
67
|
+
color: vars.compound.titleColor
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const cssShapeSvg = style({
|
|
71
|
+
top: 0,
|
|
72
|
+
left: 0,
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
pointerEvents: 'none',
|
|
75
|
+
fill: vars.element.fill,
|
|
76
|
+
stroke: vars.element.stroke,
|
|
77
|
+
overflow: 'visible',
|
|
78
|
+
filter: `
|
|
79
|
+
drop-shadow(0 2px 1px rgba(0, 0, 0, 0.21))
|
|
80
|
+
drop-shadow(0 1px 1px color-mix(in srgb, ${vars.element.stroke} 40%, transparent))
|
|
81
|
+
drop-shadow(0 5px 3px rgba(0, 0, 0, 0.1))
|
|
82
|
+
`,
|
|
83
|
+
zIndex: -1
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const DimmedTransitionDelay = '400ms'
|
|
87
|
+
|
|
88
|
+
export const edgePath = style({
|
|
89
|
+
opacity: 1,
|
|
90
|
+
transition: 'all 0.2s ease-in-out',
|
|
91
|
+
|
|
92
|
+
selectors: {
|
|
93
|
+
[`&[data-edge-dimmed='true']`]: {
|
|
94
|
+
opacity: 0.1,
|
|
95
|
+
transitionDelay: DimmedTransitionDelay
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export const edgeLabel = style({
|
|
101
|
+
padding: '2px 5px',
|
|
102
|
+
fontFamily: vars.likec4.font,
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
cursor: 'pointer',
|
|
105
|
+
width: 'fit-content',
|
|
106
|
+
color: xyvars.edge.labelColor,
|
|
107
|
+
backgroundColor: xyvars.edge.labelBgColor,
|
|
108
|
+
borderRadius: 4,
|
|
109
|
+
// everything inside EdgeLabelRenderer has no pointer events by default
|
|
110
|
+
// if you have an interactive element, set pointer-events: all
|
|
111
|
+
pointerEvents: 'all',
|
|
112
|
+
textWrap: 'pretty',
|
|
113
|
+
whiteSpace: 'preserve-breaks',
|
|
114
|
+
opacity: 1,
|
|
115
|
+
transition: 'all 0.2s ease-in-out',
|
|
116
|
+
selectors: {
|
|
117
|
+
[`&[data-edge-dimmed='true']`]: {
|
|
118
|
+
opacity: 0.1,
|
|
119
|
+
transitionDelay: DimmedTransitionDelay
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
export const edgeLabelText = style({
|
|
125
|
+
textAlign: 'center',
|
|
126
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
127
|
+
fontSize: mantine.fontSizes.md,
|
|
128
|
+
lineHeight: mantine.lineHeights.xs
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
export const edgeLabelTechnology = style({
|
|
132
|
+
textAlign: 'center',
|
|
133
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
134
|
+
fontSize: mantine.fontSizes.sm,
|
|
135
|
+
lineHeight: 1
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
export const navigateBtnBox = style({
|
|
139
|
+
zIndex: 100,
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
left: '50%',
|
|
142
|
+
bottom: 2,
|
|
143
|
+
transform: 'translate(-50%, 0%)',
|
|
144
|
+
gap: 0,
|
|
145
|
+
transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
|
|
146
|
+
selectors: {
|
|
147
|
+
[`:where([data-likec4-shape='browser'],[data-likec4-shape='mobile']) &`]: {
|
|
148
|
+
bottom: 6
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
globalStyle(`:where(${elementNode}:hover) ${navigateBtnBox}`, {
|
|
153
|
+
transitionDelay: '20ms',
|
|
154
|
+
gap: 8,
|
|
155
|
+
transform: 'translate(-50%, 5px)'
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
export const navigateBtn = style({
|
|
159
|
+
opacity: 0.7,
|
|
160
|
+
pointerEvents: 'all',
|
|
161
|
+
cursor: 'pointer',
|
|
162
|
+
transform: 'scale(0.9)',
|
|
163
|
+
transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
|
|
164
|
+
'vars': {
|
|
165
|
+
'--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
|
|
166
|
+
'--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
|
|
167
|
+
'--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
|
|
168
|
+
},
|
|
169
|
+
':hover': {
|
|
170
|
+
transitionDelay: '0ms',
|
|
171
|
+
transform: 'scale(1.25)',
|
|
172
|
+
boxShadow: mantine.shadows.lg
|
|
173
|
+
},
|
|
174
|
+
':active': {
|
|
175
|
+
transform: 'scale(0.975)'
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
globalStyle(`:where(${elementNode}:hover) ${navigateBtn}`, {
|
|
180
|
+
transitionDelay: '40ms',
|
|
181
|
+
transitionTimingFunction: 'cubic-bezier(0, 0, 0.40, 1)',
|
|
182
|
+
opacity: 1,
|
|
183
|
+
transform: 'scale(1.07)',
|
|
184
|
+
boxShadow: mantine.shadows.md,
|
|
185
|
+
vars: {
|
|
186
|
+
'--ai-bg': `var(--ai-bg-hover)`
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
// globalStyle(`${elementNode}:hover ${navigateBtn}`, {
|
|
190
|
+
// opacity: 1,
|
|
191
|
+
// transform: 'scale(1.05)',
|
|
192
|
+
// boxShadow: mantine.shadows.md,
|
|
193
|
+
// vars: {
|
|
194
|
+
// '--ai-bg': `var(--ai-bg-hover)`
|
|
195
|
+
// }
|
|
196
|
+
// })
|
package/src/overlays/index.tsx
CHANGED
|
@@ -1,33 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { useDiagramState } from '../hooks/useDiagramState'
|
|
3
|
-
import { EdgeDetailsOverlay } from './edge-details/EdgeDetailsOverlay'
|
|
4
|
-
import { RelationshipsOfOverlay } from './relationships-of/RelationshipsOverlay'
|
|
5
|
-
|
|
6
|
-
export const Overlays = memo(() => {
|
|
7
|
-
const {
|
|
8
|
-
hasLikeC4Model,
|
|
9
|
-
activeOverlay
|
|
10
|
-
} = useDiagramState(s => ({
|
|
11
|
-
hasLikeC4Model: s.hasLikeC4Model,
|
|
12
|
-
activeOverlay: s.activeOverlay
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
if (!activeOverlay) {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (!hasLikeC4Model) {
|
|
20
|
-
console.warn('LikeC4Model is not available, but overlays are active')
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (activeOverlay.relationshipsOf) {
|
|
25
|
-
return <RelationshipsOfOverlay />
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (activeOverlay.edgeDetails) {
|
|
29
|
-
return <EdgeDetailsOverlay edgeId={activeOverlay.edgeDetails} />
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return null
|
|
33
|
-
})
|
|
1
|
+
export { Overlays } from './Overlays'
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { isAncestor, type Relation } from '@likec4/core'
|
|
1
|
+
import { type Fqn, isAncestor, type Relation } from '@likec4/core'
|
|
2
2
|
import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
|
|
3
3
|
import { useLocalStorage, useStateHistory } from '@mantine/hooks'
|
|
4
|
-
import {
|
|
4
|
+
import { useDebouncedCallback, useSyncedRef } from '@react-hookz/web'
|
|
5
5
|
import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getViewportForBounds,
|
|
8
|
+
Panel,
|
|
9
|
+
ReactFlow,
|
|
10
|
+
type ReactFlowInstance,
|
|
11
|
+
useReactFlow,
|
|
12
|
+
useStoreApi
|
|
13
|
+
} from '@xyflow/react'
|
|
14
|
+
import { getNodeDimensions } from '@xyflow/system'
|
|
7
15
|
import { memo, useEffect, useRef } from 'react'
|
|
8
16
|
import { map, only, prop, unique } from 'remeda'
|
|
9
|
-
import { useOverlayDialog } from '../../components'
|
|
10
17
|
import { useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
18
|
+
import { useOverlayDialog } from '../OverlayContext'
|
|
11
19
|
import type { XYFlowTypes } from './_types'
|
|
12
20
|
import { SelectElement } from './SelectElement'
|
|
13
|
-
import * as css from './styles.css'
|
|
14
21
|
import { useLayoutedRelationships, ZIndexes } from './use-layouted-relationships'
|
|
15
22
|
import { CompoundNode } from './xyflow/CompoundNode'
|
|
16
23
|
import { ElementNode } from './xyflow/ElementNode'
|
|
@@ -26,6 +33,9 @@ const edgeTypes = {
|
|
|
26
33
|
relation: RelationshipEdge
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
const findSubjectNode = (nodes: XYFlowTypes.Node[]) =>
|
|
37
|
+
nodes.find((n): n is XYFlowTypes.ElementNode => n.type === 'element' && n.data.column === 'subject')
|
|
38
|
+
|
|
29
39
|
const resetDimmedAndHovered = (xyflow: ReactFlowInstance<XYFlowTypes.Node, XYFlowTypes.Edge>) => {
|
|
30
40
|
xyflow.setEdges(edges =>
|
|
31
41
|
edges.map(edge => ({
|
|
@@ -70,7 +80,7 @@ const onlyOneUnique = <T extends keyof Relation>(
|
|
|
70
80
|
return only(unique(map(data.relations, prop(property))))
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
83
|
+
export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function RelationshipsXYFlow({ subjectId }) {
|
|
74
84
|
const diagramStore = useDiagramStoreApi()
|
|
75
85
|
const lastClickedNodeRef = useRef<XYFlowTypes.NonEmptyNode | null>(null)
|
|
76
86
|
const [_scope, setScope] = useLocalStorage<'global' | 'view'>({
|
|
@@ -83,15 +93,16 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
|
83
93
|
viewIncludesSubject,
|
|
84
94
|
edges,
|
|
85
95
|
nodes,
|
|
86
|
-
subject
|
|
87
|
-
|
|
96
|
+
subject,
|
|
97
|
+
bounds
|
|
98
|
+
} = useLayoutedRelationships(subjectId, _scope)
|
|
88
99
|
const scope = viewIncludesSubject ? _scope : 'global'
|
|
89
100
|
const showSubjectWarning = !viewIncludesSubject && _scope === 'view'
|
|
90
101
|
|
|
91
102
|
const [historySubjectId, historyOps, { history, current }] = useStateHistory(subject.id)
|
|
92
103
|
|
|
93
104
|
const xyflow = useReactFlow<XYFlowTypes.Node, XYFlowTypes.Edge>()
|
|
94
|
-
const xystore = useStoreApi()
|
|
105
|
+
const xystore = useStoreApi<XYFlowTypes.Node, XYFlowTypes.Edge>()
|
|
95
106
|
const overlay = useOverlayDialog()
|
|
96
107
|
|
|
97
108
|
useEffect(() => {
|
|
@@ -108,66 +119,119 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
|
108
119
|
}
|
|
109
120
|
}, [historySubjectId])
|
|
110
121
|
|
|
122
|
+
const fitview = useDebouncedCallback(
|
|
123
|
+
() => {
|
|
124
|
+
xyflow.fitView({
|
|
125
|
+
padding: 0.2,
|
|
126
|
+
includeHiddenNodes: true,
|
|
127
|
+
maxZoom: 1,
|
|
128
|
+
duration: 500
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
[xyflow],
|
|
132
|
+
150
|
|
133
|
+
)
|
|
134
|
+
|
|
111
135
|
useEffect(() => {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
const {
|
|
137
|
+
nodes: _nodes,
|
|
138
|
+
edges: _edges,
|
|
139
|
+
setNodes,
|
|
140
|
+
setEdges,
|
|
141
|
+
width,
|
|
142
|
+
height
|
|
143
|
+
} = xystore.getState()
|
|
144
|
+
|
|
145
|
+
const nextSubjectNode = findSubjectNode(nodes)
|
|
146
|
+
const currentSubjectNode = findSubjectNode(_nodes)
|
|
147
|
+
|
|
148
|
+
// If subject node is the same, don't animate
|
|
149
|
+
if (nextSubjectNode?.data.fqn === currentSubjectNode?.data.fqn) {
|
|
150
|
+
setNodes(nodes)
|
|
151
|
+
setEdges(edges)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
if (!nextSubjectNode) {
|
|
116
156
|
console.error('Subject node not found')
|
|
117
157
|
} else if (nextSubjectNode.data.fqn !== subject.id) {
|
|
118
158
|
console.error(`Subject node mismatch, expected: ${subject.id} got: ${nextSubjectNode.data.fqn}`)
|
|
119
159
|
}
|
|
160
|
+
|
|
161
|
+
const nextzoom = getViewportForBounds(bounds, width, height, 0.2, 1, 0.2).zoom
|
|
162
|
+
|
|
163
|
+
const nextSubjectCenter = nextSubjectNode && {
|
|
164
|
+
x: nextSubjectNode.position.x + (nextSubjectNode.width ?? 0) / 2,
|
|
165
|
+
y: nextSubjectNode.position.y + (nextSubjectNode.height ?? 0) / 2
|
|
166
|
+
}
|
|
167
|
+
|
|
120
168
|
const existingNode = lastClickedNodeRef.current
|
|
121
169
|
?? xyflow.getNodes().find(n => n.type !== 'empty' && n.data.column !== 'subject' && n.data.fqn === subject.id)
|
|
122
170
|
lastClickedNodeRef.current = null
|
|
123
|
-
//
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
// Animate from existing node to next subject node
|
|
172
|
+
if (nextSubjectCenter && existingNode) {
|
|
173
|
+
// Dim all nodes except the existing node
|
|
174
|
+
setNodes(_nodes.map(n => {
|
|
175
|
+
return {
|
|
176
|
+
...n,
|
|
177
|
+
data: {
|
|
178
|
+
...n.data,
|
|
179
|
+
dimmed: n.id !== existingNode.id ? 'immediate' : false
|
|
180
|
+
}
|
|
181
|
+
} as XYFlowTypes.Node
|
|
182
|
+
}))
|
|
183
|
+
setEdges(_edges.map(e => ({
|
|
184
|
+
...e,
|
|
185
|
+
data: {
|
|
186
|
+
...e.data,
|
|
187
|
+
dimmed: true
|
|
137
188
|
}
|
|
138
|
-
|
|
139
|
-
xystore.getState().setNodes(nodes)
|
|
140
|
-
xystore.getState().setEdges(edges)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
xyflow.setNodes(nodes)
|
|
144
|
-
xyflow.setEdges(edges)
|
|
145
|
-
}, [nodes, edges])
|
|
189
|
+
})))
|
|
146
190
|
|
|
147
|
-
|
|
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
|
+
}
|
|
148
198
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
// Pick the smaller zoom level
|
|
200
|
+
const zoom = Math.min(
|
|
201
|
+
xyflow.getViewport().zoom,
|
|
202
|
+
nextzoom,
|
|
203
|
+
nextSubjectNode.width! / existingDimensions.width,
|
|
204
|
+
nextSubjectNode.height! / existingDimensions.height
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
let isCancelled = false
|
|
208
|
+
|
|
209
|
+
xyflow.setCenter(existingCenter.x, existingCenter.y, { zoom, duration: 350 }).then(() => {
|
|
210
|
+
// If the component is unmounted, we don't want to update the state
|
|
211
|
+
if (isCancelled) return
|
|
212
|
+
setNodes(nodes)
|
|
213
|
+
setEdges(edges)
|
|
214
|
+
xyflow.setCenter(nextSubjectCenter.x, nextSubjectCenter.y, { zoom: nextzoom })
|
|
215
|
+
fitview()
|
|
156
216
|
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
217
|
+
return () => {
|
|
218
|
+
isCancelled = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
setNodes(nodes)
|
|
222
|
+
setEdges(edges)
|
|
223
|
+
fitview()
|
|
224
|
+
return undefined
|
|
225
|
+
}, [nodes, edges])
|
|
161
226
|
|
|
162
227
|
return (
|
|
163
228
|
<ReactFlow
|
|
164
|
-
className={css.root}
|
|
165
229
|
defaultEdges={[] as XYFlowTypes.Edge[]}
|
|
166
230
|
defaultNodes={[] as XYFlowTypes.Node[]}
|
|
167
231
|
nodeTypes={nodeTypes}
|
|
168
232
|
edgeTypes={edgeTypes}
|
|
169
233
|
defaultMarkerColor="var(--xy-edge-stroke)"
|
|
170
|
-
zoomOnPinch={
|
|
234
|
+
zoomOnPinch={true}
|
|
171
235
|
zoomOnScroll={false}
|
|
172
236
|
zoomOnDoubleClick={false}
|
|
173
237
|
maxZoom={1.5}
|
|
@@ -224,30 +288,27 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
|
224
288
|
onNodeClick={(e, node) => {
|
|
225
289
|
e.stopPropagation()
|
|
226
290
|
if (node.type === 'empty') return
|
|
227
|
-
|
|
228
|
-
lastClickedNodeRef.current = node
|
|
229
|
-
}
|
|
291
|
+
lastClickedNodeRef.current = node
|
|
230
292
|
overlay.openOverlay({
|
|
231
293
|
relationshipsOf: node.data.fqn
|
|
232
294
|
})
|
|
233
295
|
}}
|
|
296
|
+
onNodeDoubleClick={(e) => {
|
|
297
|
+
e.stopPropagation()
|
|
298
|
+
}}
|
|
234
299
|
onDoubleClick={e => {
|
|
235
300
|
e.stopPropagation()
|
|
236
|
-
|
|
237
|
-
includeHiddenNodes: true,
|
|
238
|
-
maxZoom: 1,
|
|
239
|
-
duration: 450
|
|
240
|
-
})
|
|
301
|
+
fitview()
|
|
241
302
|
}}
|
|
242
303
|
onEdgeClick={(e, edge) => {
|
|
243
304
|
e.stopPropagation()
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
305
|
+
// One relation in edge
|
|
306
|
+
const relationId = only(edge.data.relations)?.id
|
|
307
|
+
if (relationId) {
|
|
308
|
+
diagramStore.getState().onOpenSourceRelation?.(relationId)
|
|
249
309
|
return
|
|
250
310
|
}
|
|
311
|
+
|
|
251
312
|
const nodeId = onlyOneUnique(edge.data, 'source') ? edge.source : edge.target
|
|
252
313
|
const next = xyflow.getNode(nodeId)
|
|
253
314
|
if (next && next.type !== 'empty') {
|
|
@@ -285,7 +346,7 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
|
285
346
|
</Button>
|
|
286
347
|
<Space w={2} />
|
|
287
348
|
<Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
|
|
288
|
-
<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>
|
|
289
350
|
<Box flex={'1 0 auto'}>
|
|
290
351
|
<SelectElement
|
|
291
352
|
scope={scope}
|
|
@@ -324,7 +385,7 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
|
|
|
324
385
|
}}
|
|
325
386
|
>
|
|
326
387
|
<Text fw={500} size="xs" c="orange" component="div">
|
|
327
|
-
Current view doesn't include
|
|
388
|
+
Current view doesn't include this element, switched to Global
|
|
328
389
|
</Text>
|
|
329
390
|
</Box>
|
|
330
391
|
)}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ComputedNode, Fqn, Relation, ViewID } from '@likec4/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Edge as ReactFlowEdge,
|
|
4
|
+
InternalNode as ReactFlowInternalNode,
|
|
5
|
+
Node as ReactFlowNode,
|
|
6
|
+
ReactFlowInstance
|
|
7
|
+
} from '@xyflow/react'
|
|
3
8
|
import type { SetRequired } from 'type-fest'
|
|
4
9
|
|
|
5
10
|
export namespace XYFlowTypes {
|
|
@@ -23,7 +28,11 @@ export namespace XYFlowTypes {
|
|
|
23
28
|
}
|
|
24
29
|
navigateTo: ViewID | null
|
|
25
30
|
hovered?: boolean
|
|
26
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Whether the node is dimmed
|
|
33
|
+
* 'immediate' means that the node is dimmed without delay
|
|
34
|
+
*/
|
|
35
|
+
dimmed?: 'immediate' | boolean
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export type ElementNode = SetRequired<ReactFlowNode<NodeProps, 'element'>, 'type'>
|
|
@@ -53,4 +62,6 @@ export namespace XYFlowTypes {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export type Instance = ReactFlowInstance<Node, Edge>
|
|
65
|
+
|
|
66
|
+
export type InternalNode = ReactFlowInternalNode<Node>
|
|
56
67
|
}
|