@likec4/diagram 1.27.1 → 1.27.2

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.
Files changed (115) hide show
  1. package/bundle/index.js +2147 -6730
  2. package/dist/LikeC4Diagram.js +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +5 -3
  4. package/dist/StaticLikeC4Diagram.js +1 -1
  5. package/dist/_virtual/set.js +5 -0
  6. package/dist/_virtual/set2.js +3 -0
  7. package/dist/base/BaseXYFlow.d.ts +2 -1
  8. package/dist/base/primitives/compound/CompoundNodeContainer.d.ts +1 -1
  9. package/dist/base/primitives/compound/CompoundNodeContainer.js +0 -1
  10. package/dist/base/primitives/element/ElementActionButtons.js +5 -10
  11. package/dist/base/primitives/element/ElementNodeContainer.css.js +4 -4
  12. package/dist/base/updateEdges.js +1 -9
  13. package/dist/base/updateNodes.js +1 -12
  14. package/dist/bundle/LikeC4View.js +2 -2
  15. package/dist/bundle/LikeC4View.props.d.ts +7 -7
  16. package/dist/hooks/useCurrentViewId.js +8 -2
  17. package/dist/hooks/useDiagram.d.ts +1517 -684
  18. package/dist/hooks/useDiagram.js +16 -8
  19. package/dist/likec4diagram/ui/notation/NotationPanel.css.js +3 -3
  20. package/dist/likec4diagram/ui/notation/NotationPanel.js +10 -4
  21. package/dist/node_modules/.pnpm/mnemonist@0.40.3_patch_hash_5c12abb43fcff6fd81e53021b89075d53249640d165a2556cc1e58d71201a13a/node_modules/mnemonist/set.js +369 -0
  22. package/dist/overlays/Overlays.js +3 -1
  23. package/dist/overlays/element-details/actor.d.ts +2769 -173
  24. package/dist/overlays/overlay/Overlay.css.d.ts +26 -2
  25. package/dist/overlays/overlay/Overlay.css.js +66 -18
  26. package/dist/overlays/overlay/Overlay.d.ts +7 -6
  27. package/dist/overlays/overlay/Overlay.js +12 -8
  28. package/dist/overlays/overlaysActor.js +2 -2
  29. package/dist/overlays/relationship-details/RelationshipDetails.js +113 -70
  30. package/dist/overlays/relationship-details/SelectEdge.d.ts +1 -1
  31. package/dist/overlays/relationship-details/SelectEdge.js +15 -9
  32. package/dist/overlays/relationship-details/_types.d.ts +4 -1
  33. package/dist/overlays/relationship-details/actor.d.ts +415 -128
  34. package/dist/overlays/relationship-details/actor.js +162 -53
  35. package/dist/overlays/relationship-details/compute.d.ts +12 -0
  36. package/dist/overlays/relationship-details/compute.js +139 -0
  37. package/dist/overlays/relationship-details/custom/ElementActions.js +27 -5
  38. package/dist/overlays/relationship-details/custom/edgeTypes.js +1 -1
  39. package/dist/overlays/relationship-details/custom/nodeTypes.d.ts +2 -1
  40. package/dist/overlays/relationship-details/hooks.d.ts +336 -6
  41. package/dist/overlays/relationship-details/hooks.js +9 -7
  42. package/dist/overlays/relationship-details/layout-to-xyflow.d.ts +12 -0
  43. package/dist/overlays/relationship-details/layout-to-xyflow.js +149 -0
  44. package/dist/overlays/relationship-details/layout.d.ts +22 -0
  45. package/dist/overlays/relationship-details/layout.js +314 -0
  46. package/dist/overlays/relationships-browser/RelationshipsBrowser.d.ts +3 -1
  47. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +2 -2
  48. package/dist/overlays/relationships-browser/_types.d.ts +2 -0
  49. package/dist/overlays/relationships-browser/actor.d.ts +130 -14
  50. package/dist/overlays/relationships-browser/actor.js +30 -16
  51. package/dist/overlays/relationships-browser/custom/ElementActions.js +5 -3
  52. package/dist/overlays/relationships-browser/custom/edgeTypes.d.ts +3 -1
  53. package/dist/overlays/relationships-browser/custom/edgeTypes.js +1 -1
  54. package/dist/overlays/relationships-browser/hooks.d.ts +3 -1
  55. package/dist/overlays/relationships-browser/hooks.js +1 -1
  56. package/dist/overlays/relationships-browser/{-useRelationshipsView.d.ts → layout.d.ts} +2 -0
  57. package/dist/overlays/relationships-browser/{-useRelationshipsView.js → layout.js} +19 -2
  58. package/dist/overlays/relationships-browser/useViewToNodesEdges.d.ts +1 -1
  59. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +3 -1
  60. package/dist/packages/core/src/compute-view/relationships-view/compute.js +1 -1
  61. package/dist/packages/core/src/model/connection/model/ConnectionModel.js +121 -0
  62. package/dist/packages/core/src/model/connection/model/find.js +39 -0
  63. package/dist/packages/core/src/model/connection/ops.js +7 -0
  64. package/dist/packages/core/src/utils/const.js +3 -0
  65. package/dist/packages/core/src/utils/fqn.js +9 -1
  66. package/dist/packages/core/src/utils/iterable/find.js +21 -0
  67. package/dist/packages/core/src/utils/iterable/some.js +21 -0
  68. package/dist/packages/core/src/utils/set.js +48 -0
  69. package/dist/packages/core/src/utils/string-hash.js +13 -0
  70. package/dist/state/diagram-machine.d.ts +1552 -703
  71. package/dist/state/diagram-machine.js +39 -3
  72. package/dist/state/syncManualLayoutActor.d.ts +1 -1
  73. package/dist/styles.css.js +1 -1
  74. package/package.json +11 -11
  75. package/src/LikeC4Diagram.props.ts +7 -3
  76. package/src/LikeC4Diagram.tsx +1 -1
  77. package/src/StaticLikeC4Diagram.tsx +1 -1
  78. package/src/base/BaseXYFlow.tsx +2 -1
  79. package/src/base/primitives/compound/CompoundNodeContainer.tsx +0 -2
  80. package/src/base/primitives/element/ElementActionButtons.tsx +6 -7
  81. package/src/base/primitives/element/ElementNodeContainer.css.ts +4 -4
  82. package/src/base/updateEdges.ts +8 -8
  83. package/src/base/updateNodes.ts +11 -11
  84. package/src/bundle/LikeC4View.props.ts +7 -7
  85. package/src/bundle/LikeC4View.tsx +2 -2
  86. package/src/hooks/useCurrentViewId.ts +4 -2
  87. package/src/hooks/useDiagram.ts +18 -9
  88. package/src/likec4diagram/ui/notation/NotationPanel.css.ts +2 -2
  89. package/src/likec4diagram/ui/notation/NotationPanel.tsx +8 -6
  90. package/src/overlays/Overlays.tsx +3 -1
  91. package/src/overlays/overlay/Overlay.css.ts +67 -17
  92. package/src/overlays/overlay/Overlay.tsx +26 -21
  93. package/src/overlays/overlaysActor.ts +1 -1
  94. package/src/overlays/relationship-details/RelationshipDetails.tsx +131 -68
  95. package/src/overlays/relationship-details/SelectEdge.tsx +19 -12
  96. package/src/overlays/relationship-details/_types.ts +3 -1
  97. package/src/overlays/relationship-details/actor.ts +217 -68
  98. package/src/overlays/relationship-details/compute.ts +186 -0
  99. package/src/overlays/relationship-details/custom/ElementActions.tsx +27 -18
  100. package/src/overlays/relationship-details/custom/edgeTypes.tsx +1 -1
  101. package/src/overlays/relationship-details/hooks.ts +10 -8
  102. package/src/overlays/relationship-details/layout-to-xyflow.ts +195 -0
  103. package/src/overlays/relationship-details/layout.ts +470 -0
  104. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +2 -2
  105. package/src/overlays/relationships-browser/_types.ts +2 -0
  106. package/src/overlays/relationships-browser/actor.ts +39 -14
  107. package/src/overlays/relationships-browser/custom/ElementActions.tsx +5 -3
  108. package/src/overlays/relationships-browser/custom/edgeTypes.tsx +1 -1
  109. package/src/overlays/relationships-browser/hooks.ts +2 -2
  110. package/src/overlays/relationships-browser/{-useRelationshipsView.ts → layout.ts} +23 -13
  111. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +3 -1
  112. package/src/state/diagram-machine.ts +51 -6
  113. package/dist/overlays/relationship-details/useLayoutedDetails.d.ts +0 -19
  114. package/dist/overlays/relationship-details/useLayoutedDetails.js +0 -267
  115. package/src/overlays/relationship-details/useLayoutedDetails.ts +0 -376
@@ -1,16 +1,29 @@
1
- import { type BBox, type DiagramView, type EdgeId, invariant } from '@likec4/core'
1
+ import {
2
+ type BBox,
3
+ type DiagramView,
4
+ type EdgeId,
5
+ type ExclusiveUnion,
6
+ type Fqn,
7
+ type ViewId,
8
+ invariant,
9
+ } from '@likec4/core'
2
10
  import {
3
11
  type EdgeChange,
4
12
  type NodeChange,
5
13
  type ReactFlowInstance,
14
+ type useStoreApi,
6
15
  applyEdgeChanges,
7
16
  applyNodeChanges,
17
+ getViewportForBounds,
8
18
  } from '@xyflow/react'
9
- import { prop } from 'remeda'
19
+ import type { InternalNodeUpdate } from '@xyflow/system'
20
+ import { shallowEqual } from 'fast-equals'
21
+ import { isString, prop } from 'remeda'
10
22
  import {
11
23
  type ActorLogicFrom,
12
24
  type ActorRefFromLogic,
13
25
  type SnapshotFrom,
26
+ assertEvent,
14
27
  assign,
15
28
  cancel,
16
29
  enqueueActions,
@@ -19,30 +32,68 @@ import {
19
32
  } from 'xstate'
20
33
  import { Base } from '../../base'
21
34
  import { MinZoom } from '../../base/const'
35
+ import { updateEdges } from '../../base/updateEdges'
36
+ import { updateNodes } from '../../base/updateNodes'
22
37
  import type { RelationshipDetailsTypes } from './_types'
38
+ import type { LayoutResult } from './layout'
39
+ import { layoutResultToXYFlow } from './layout-to-xyflow'
23
40
 
24
41
  type XYFLowInstance = ReactFlowInstance<RelationshipDetailsTypes.Node, RelationshipDetailsTypes.Edge>
42
+ type XYStoreApi = ReturnType<typeof useStoreApi<RelationshipDetailsTypes.Node, RelationshipDetailsTypes.Edge>>
43
+
44
+ export type Input = ExclusiveUnion<{
45
+ Edge: {
46
+ edgeId: EdgeId
47
+ viewId: ViewId
48
+ }
49
+ Between: {
50
+ source: Fqn
51
+ target: Fqn
52
+ viewId: ViewId
53
+ }
54
+ }>
25
55
 
26
- export type Input = {
56
+ type Subject = {
27
57
  edgeId: EdgeId
28
- view: DiagramView
29
- // view: DiagramView | null
30
- // parentRef?: AnyActorRef| null
58
+ // relationships: null
59
+ } | {
60
+ source: Fqn
61
+ target: Fqn
62
+ edgeId?: never
63
+ // relationships: null
31
64
  }
32
65
 
33
- export type Context = Readonly<
34
- Input & {
35
- // parentRef: AnyActorRef | null
36
- xyflow: XYFLowInstance | null
37
- initialized: boolean
38
- xynodes: RelationshipDetailsTypes.Node[]
39
- xyedges: RelationshipDetailsTypes.Edge[]
40
- // bounds: BBox | null
66
+ export type Context = Readonly<{
67
+ // parentRef: AnyActorRef | null
68
+ subject: Subject
69
+ // Scope
70
+ viewId: ViewId
71
+ xyflow: XYFLowInstance | null
72
+ xystore: XYStoreApi | null
73
+ initialized: {
74
+ xydata: boolean
75
+ xyflow: boolean
76
+ }
77
+ xynodes: RelationshipDetailsTypes.Node[]
78
+ xyedges: RelationshipDetailsTypes.Edge[]
79
+ bounds: BBox
80
+ }>
81
+
82
+ function inputToSubject(input: { edgeId: EdgeId } | { source: Fqn; target: Fqn }): Context['subject'] {
83
+ if ('edgeId' in input) {
84
+ invariant(isString(input.edgeId), 'edgeId is required')
85
+ return {
86
+ edgeId: input.edgeId,
87
+ }
88
+ }
89
+ return {
90
+ source: input.source,
91
+ target: input.target,
41
92
  }
42
- >
93
+ }
43
94
 
44
95
  export type Events =
45
- | { type: 'xyflow.init'; instance: XYFLowInstance }
96
+ | { type: 'xyflow.init'; instance: XYFLowInstance; store: XYStoreApi }
46
97
  | { type: 'xyflow.nodeClick'; node: RelationshipDetailsTypes.Node }
47
98
  | { type: 'xyflow.edgeClick'; edge: RelationshipDetailsTypes.Edge }
48
99
  | { type: 'xyflow.edgeMouseEnter'; edge: RelationshipDetailsTypes.Edge }
@@ -53,15 +104,16 @@ export type Events =
53
104
  | { type: 'xyflow.applyNodeChanges'; changes: NodeChange<RelationshipDetailsTypes.Node>[] }
54
105
  | { type: 'xyflow.applyEdgeChanges'; changes: EdgeChange<RelationshipDetailsTypes.Edge>[] }
55
106
  | { type: 'xyflow.paneClick' }
107
+ | { type: 'xyflow.paneDblClick' }
56
108
  | { type: 'xyflow.resized' }
109
+ | { type: 'xyflow.updateNodeInternals' }
57
110
  | {
58
- type: 'update.xydata'
59
- xynodes: RelationshipDetailsTypes.Node[]
60
- xyedges: RelationshipDetailsTypes.Edge[]
111
+ type: 'update.layoutData'
112
+ data: LayoutResult
61
113
  }
62
114
  // | { type: 'update.bounds'; bounds: BBox }
63
115
  | { type: 'fitDiagram'; duration?: number; bounds?: BBox }
64
- | { type: 'navigate.to'; edgeId: EdgeId }
116
+ | { type: 'navigate.to'; params: { edgeId: EdgeId; viewId?: ViewId } | { source: Fqn; target: Fqn; viewId?: ViewId } }
65
117
  | { type: 'close' }
66
118
 
67
119
  export const relationshipDetailsLogic = setup({
@@ -72,16 +124,18 @@ export const relationshipDetailsLogic = setup({
72
124
  },
73
125
  actions: {
74
126
  'xyflow:fitDiagram': ({ context }, params?: { duration?: number; bounds?: BBox }) => {
75
- const {
76
- duration = 450,
77
- bounds,
78
- } = params ?? {}
79
- const { xyflow } = context
127
+ let { duration, bounds } = params ?? {}
128
+ duration ??= 450
129
+ const { xyflow, xystore } = context
80
130
  invariant(xyflow, 'xyflow is not initialized')
131
+ invariant(xystore, 'xystore is not initialized')
132
+ bounds ??= context.bounds
133
+ const maxZoom = Math.max(xyflow.getZoom(), 1)
81
134
  if (bounds) {
82
- xyflow.fitBounds(bounds, duration > 0 ? { duration } : undefined)
135
+ const { width, height } = xystore.getState()
136
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, 0.1)
137
+ xyflow.setViewport(viewport, duration > 0 ? { duration } : undefined)
83
138
  } else {
84
- const maxZoom = Math.max(xyflow.getZoom(), 1)
85
139
  xyflow.fitView({
86
140
  minZoom: MinZoom,
87
141
  maxZoom,
@@ -90,45 +144,120 @@ export const relationshipDetailsLogic = setup({
90
144
  })
91
145
  }
92
146
  },
147
+ 'xyflow:updateNodeInternals': ({ context }) => {
148
+ invariant(context.xystore, 'xystore is not initialized')
149
+ const { domNode, updateNodeInternals } = context.xystore.getState()
150
+ const nodeIds = new Set(context.xyedges.flatMap((e) => [e.source, e.target]))
151
+
152
+ if (nodeIds.size === 0 || !domNode) {
153
+ return
154
+ }
155
+
156
+ const updates = new Map<string, InternalNodeUpdate>()
157
+
158
+ for (const updateId of nodeIds) {
159
+ const nodeElement = domNode.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement
160
+ if (nodeElement) {
161
+ updates.set(updateId, { id: updateId, nodeElement, force: true })
162
+ }
163
+ }
164
+
165
+ updateNodeInternals(updates, { triggerFitView: false })
166
+ },
167
+ 'updateXYFlow': assign(({ context, event }) => {
168
+ assertEvent(event, 'xyflow.init')
169
+ let initialized = context.initialized
170
+ if (!initialized.xyflow) {
171
+ initialized = {
172
+ ...initialized,
173
+ xyflow: true,
174
+ }
175
+ }
176
+ return {
177
+ initialized,
178
+ xyflow: event.instance,
179
+ xystore: event.store,
180
+ }
181
+ }),
182
+ 'updateLayoutData': assign(({ context, event }) => {
183
+ assertEvent(event, 'update.layoutData')
184
+ const { xynodes, xyedges, bounds } = layoutResultToXYFlow(event.data)
185
+ let initialized = context.initialized
186
+ if (!initialized.xydata) {
187
+ initialized = {
188
+ ...initialized,
189
+ xydata: true,
190
+ }
191
+ }
192
+ return {
193
+ initialized,
194
+ xynodes: updateNodes(context.xynodes, xynodes),
195
+ xyedges: updateEdges(context.xyedges, xyedges),
196
+ bounds: shallowEqual(context.bounds, bounds) ? context.bounds : bounds,
197
+ }
198
+ }),
93
199
  },
94
200
  guards: {
201
+ 'isReady': ({ context }) => context.initialized.xydata && context.initialized.xyflow,
95
202
  'enable: navigate.to': () => true,
96
203
  },
97
204
  }).createMachine({
98
- initial: 'opening',
205
+ initial: 'initializing',
99
206
  context: ({ input }) => ({
100
- ...input,
101
- initialized: false,
207
+ subject: inputToSubject(input),
208
+ viewId: input.viewId,
209
+ bounds: {
210
+ x: 0,
211
+ y: 0,
212
+ width: 200,
213
+ height: 200,
214
+ },
215
+ initialized: {
216
+ xydata: false,
217
+ xyflow: false,
218
+ },
102
219
  xyflow: null,
220
+ xystore: null,
103
221
  xynodes: [],
104
222
  xyedges: [],
105
223
  }),
106
224
  states: {
107
- 'opening': {
225
+ 'initializing': {
108
226
  on: {
109
227
  'xyflow.init': {
110
- actions: assign({
111
- initialized: true,
112
- xyflow: ({ event }) => event.instance,
113
- }),
114
- target: 'active',
228
+ actions: 'updateXYFlow',
229
+ target: 'isReady',
230
+ },
231
+ 'update.layoutData': {
232
+ actions: 'updateLayoutData',
233
+ target: 'isReady',
115
234
  },
116
235
  'close': {
117
236
  target: 'closed',
118
237
  },
119
238
  },
120
239
  },
121
- 'active': {
122
- entry: {
123
- type: 'xyflow:fitDiagram',
124
- params: { duration: 0 },
125
- },
240
+ 'isReady': {
241
+ always: [{
242
+ guard: 'isReady',
243
+ actions: [
244
+ { type: 'xyflow:fitDiagram', params: { duration: 0 } },
245
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 50 }),
246
+ ],
247
+ target: 'ready',
248
+ }, {
249
+ target: 'initializing',
250
+ }],
251
+ },
252
+ 'ready': {
126
253
  on: {
127
254
  'xyflow.edgeMouseEnter': {
128
255
  actions: [
129
256
  assign({
130
257
  xyedges: ({ context, event }) => {
131
- const hasDimmed = context.xyedges.some(edge => edge.data.dimmed === true)
258
+ const hasDimmed = context.xyedges.some(edge =>
259
+ edge.data.dimmed === true || edge.data.dimmed === 'immediate'
260
+ )
132
261
  return context.xyedges.map(edge => {
133
262
  if (edge.id === event.edge.id) {
134
263
  return Base.setData(edge, {
@@ -136,7 +265,7 @@ export const relationshipDetailsLogic = setup({
136
265
  dimmed: false,
137
266
  })
138
267
  }
139
- return hasDimmed && !edge.data.dimmed && !edge.selected ? Base.setDimmed(edge, 'immediate') : edge
268
+ return hasDimmed && !edge.selected ? Base.setDimmed(edge, 'immediate') : edge
140
269
  })
141
270
  },
142
271
  }),
@@ -188,21 +317,58 @@ export const relationshipDetailsLogic = setup({
188
317
  }
189
318
  }),
190
319
  },
191
- 'navigate.to': {
320
+ 'update.layoutData': {
192
321
  actions: [
193
- assign({
194
- edgeId: ({ event }) => event.edgeId,
195
- }),
196
- raise({ type: 'fitDiagram' }, { delay: 50 }),
322
+ 'updateLayoutData',
323
+ cancel('fitDiagram'),
324
+ {
325
+ type: 'xyflow:fitDiagram',
326
+ params: {
327
+ duration: 0,
328
+ },
329
+ },
330
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 75 }),
197
331
  ],
198
332
  },
333
+ 'xyflow.init': {
334
+ actions: 'updateXYFlow',
335
+ },
336
+ 'xyflow.applyNodeChanges': {
337
+ actions: assign({
338
+ xynodes: ({ context, event }) => {
339
+ return applyNodeChanges(event.changes, context.xynodes)
340
+ },
341
+ }),
342
+ },
343
+ 'xyflow.applyEdgeChanges': {
344
+ actions: assign({
345
+ xyedges: ({ context, event }) => {
346
+ return applyEdgeChanges(event.changes, context.xyedges)
347
+ },
348
+ }),
349
+ },
350
+ 'xyflow.paneDblClick': {
351
+ actions: 'xyflow:fitDiagram',
352
+ },
353
+ 'navigate.to': {
354
+ actions: assign({
355
+ subject: ({ event }) => inputToSubject(event.params),
356
+ viewId: ({ context, event }) => event.params.viewId ?? context.viewId,
357
+ }),
358
+ },
199
359
  'close': {
200
360
  target: 'closed',
201
361
  },
202
362
  },
203
363
  exit: assign({
204
- initialized: false,
364
+ xyedges: [],
365
+ xynodes: [],
366
+ initialized: {
367
+ xydata: false,
368
+ xyflow: false,
369
+ },
205
370
  xyflow: null,
371
+ xystore: null,
206
372
  }),
207
373
  },
208
374
  'closed': {
@@ -222,25 +388,8 @@ export const relationshipDetailsLogic = setup({
222
388
  raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 200 }),
223
389
  ],
224
390
  },
225
- 'update.xydata': {
226
- actions: assign({
227
- xynodes: ({ event }) => event.xynodes,
228
- xyedges: ({ event }) => event.xyedges,
229
- }),
230
- },
231
- 'xyflow.applyNodeChanges': {
232
- actions: assign({
233
- xynodes: ({ context, event }) => {
234
- return applyNodeChanges(event.changes, context.xynodes)
235
- },
236
- }),
237
- },
238
- 'xyflow.applyEdgeChanges': {
239
- actions: assign({
240
- xyedges: ({ context, event }) => {
241
- return applyEdgeChanges(event.changes, context.xyedges)
242
- },
243
- }),
391
+ 'xyflow.updateNodeInternals': {
392
+ actions: 'xyflow:updateNodeInternals',
244
393
  },
245
394
  },
246
395
  })
@@ -0,0 +1,186 @@
1
+ import {
2
+ type EdgeId,
3
+ type Fqn,
4
+ type NonEmptyReadonlyArray,
5
+ type RelationId,
6
+ hasAtLeast,
7
+ invariant,
8
+ isAncestor,
9
+ sortParentsFirst,
10
+ } from '@likec4/core'
11
+ import { treeFromElements } from '@likec4/core/compute-view/relationships'
12
+ import {
13
+ type AnyAux,
14
+ type DeploymentRelationModel,
15
+ type ElementModel,
16
+ type LikeC4Model,
17
+ type LikeC4ViewModel,
18
+ type RelationshipModel,
19
+ modelConnection,
20
+ } from '@likec4/core/model'
21
+
22
+ export interface RelationshipDetailsViewData {
23
+ sources: ReadonlySet<ElementModel>
24
+ relationships: ReadonlySet<RelationshipModel>
25
+ targets: ReadonlySet<ElementModel>
26
+ }
27
+
28
+ const finalize = <M extends AnyAux>(
29
+ elements: Set<ElementModel<M>>,
30
+ explicits: Set<ElementModel<M>>,
31
+ ): Set<ElementModel<M>> => {
32
+ if (elements.size > 2 && explicits.size !== elements.size) {
33
+ return new Set(sortParentsFirst([
34
+ ...treeFromElements(elements).flatten(),
35
+ ...explicits,
36
+ ]))
37
+ }
38
+ if (elements.size > 1) {
39
+ return new Set(sortParentsFirst([...elements]))
40
+ }
41
+ return elements
42
+ }
43
+
44
+ export function computeEdgeDetailsViewData(
45
+ edges: NonEmptyReadonlyArray<EdgeId>,
46
+ view: LikeC4ViewModel<AnyAux>,
47
+ ): RelationshipDetailsViewData {
48
+ const sources = new Set<ElementModel>()
49
+ const relationships = new Set<RelationshipModel>()
50
+ const targets = new Set<ElementModel>()
51
+
52
+ const explicit = {
53
+ sources: new Set<ElementModel>(),
54
+ targets: new Set<ElementModel>(),
55
+ }
56
+
57
+ const addExplicit = (el: ElementModel, type: 'source' | 'target') => {
58
+ if (type === 'source') {
59
+ sources.add(el)
60
+ explicit.sources.add(el)
61
+ } else {
62
+ targets.add(el)
63
+ explicit.targets.add(el)
64
+ }
65
+ }
66
+
67
+ for (const edgeId of edges) {
68
+ const edge = view.findEdge(edgeId)
69
+ const _relationships = edge ? [...edge.relationships('model')] : []
70
+ if (!edge || !hasAtLeast(_relationships, 1) || !edge.source.hasElement() || !edge.target.hasElement()) {
71
+ continue
72
+ }
73
+ const source = edge.source.element
74
+ const target = edge.target.element
75
+ addExplicit(source, 'source')
76
+ addExplicit(target, 'target')
77
+
78
+ for (const relationship of _relationships) {
79
+ relationships.add(relationship)
80
+
81
+ if (relationship.source !== source) {
82
+ addExplicit(relationship.source, 'source')
83
+ for (const parent of relationship.source.ancestors()) {
84
+ if (parent === source) {
85
+ break
86
+ }
87
+ sources.add(parent)
88
+ }
89
+ }
90
+
91
+ if (relationship.target !== target) {
92
+ addExplicit(relationship.target, 'target')
93
+ for (const parent of relationship.target.ancestors()) {
94
+ if (parent === target) {
95
+ break
96
+ }
97
+ targets.add(parent)
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return {
104
+ sources: finalize(sources, explicit.sources),
105
+ targets: finalize(targets, explicit.targets),
106
+ relationships,
107
+ }
108
+ }
109
+
110
+ export function computeRelationshipDetailsViewData({
111
+ // relationships: _relationships,
112
+ source,
113
+ target,
114
+ }: {
115
+ source: ElementModel
116
+ target: ElementModel
117
+ // relationships: NonEmptyReadonlyArray<RelationId>
118
+ }): RelationshipDetailsViewData {
119
+ const sources = new Set<ElementModel>()
120
+ const relationships = new Set<RelationshipModel>()
121
+ const targets = new Set<ElementModel>()
122
+
123
+ const explicit = {
124
+ sources: new Set<ElementModel>(),
125
+ targets: new Set<ElementModel>(),
126
+ }
127
+
128
+ const addExplicit = (el: ElementModel, type: 'source' | 'target') => {
129
+ if (type === 'source') {
130
+ sources.add(el)
131
+ explicit.sources.add(el)
132
+ } else {
133
+ targets.add(el)
134
+ explicit.targets.add(el)
135
+ }
136
+ }
137
+
138
+ if (source) {
139
+ addExplicit(source, 'source')
140
+ }
141
+ if (target) {
142
+ addExplicit(target, 'target')
143
+ }
144
+ const [connection] = modelConnection.findConnection(source, target, 'directed')
145
+ if (!connection) {
146
+ return {
147
+ sources,
148
+ targets,
149
+ relationships,
150
+ }
151
+ }
152
+
153
+ for (const relationship of connection.relations) {
154
+ const relationSource = relationship.source
155
+ const relationTarget = relationship.target
156
+ addExplicit(relationSource, 'source')
157
+ addExplicit(relationTarget, 'target')
158
+ relationships.add(relationship)
159
+
160
+ if (source !== relationSource) {
161
+ invariant(isAncestor(source, relationSource), `${source.id} is not an ancestor of ${relationSource.id}`)
162
+ for (const parent of relationSource.ancestors()) {
163
+ if (parent === source) {
164
+ break
165
+ }
166
+ sources.add(parent)
167
+ }
168
+ }
169
+
170
+ if (target !== relationTarget) {
171
+ invariant(isAncestor(target, relationTarget), `${target.id} is not an ancestor of ${relationTarget.id}`)
172
+ for (const parent of relationTarget.ancestors()) {
173
+ if (parent === target) {
174
+ break
175
+ }
176
+ targets.add(parent)
177
+ }
178
+ }
179
+ }
180
+
181
+ return {
182
+ sources: finalize(sources, explicit.sources),
183
+ targets: finalize(targets, explicit.targets),
184
+ relationships,
185
+ }
186
+ }
@@ -1,21 +1,20 @@
1
- import { IconZoomScan } from '@tabler/icons-react'
1
+ import { IconFileSymlink, IconTransform, IconZoomScan } from '@tabler/icons-react'
2
2
  import { ElementActionButtons } from '../../../base/primitives'
3
3
  import type { NodeProps } from '../../../base/types'
4
- import { useEnabledFeature } from '../../../context/DiagramFeatures'
4
+ import { useEnabledFeatures } from '../../../context/DiagramFeatures'
5
+ import { useCurrentViewId } from '../../../hooks/useCurrentViewId'
5
6
  import { useDiagram } from '../../../hooks/useDiagram'
6
7
  import type { RelationshipDetailsTypes as Types } from '../_types'
7
- import { useRelationshipDetails } from '../hooks'
8
- // import { useRelationshipsBrowser } from '../hooks'
9
8
 
10
9
  type ElementActionsProps = NodeProps<Types.ElementNodeData>
11
10
  export const ElementActions = (props: ElementActionsProps) => {
12
- const { enableNavigateTo } = useEnabledFeature('NavigateTo')
11
+ const { enableNavigateTo, enableVscode } = useEnabledFeatures()
13
12
  const diagram = useDiagram()
14
-
13
+ const currentViewId = useCurrentViewId()
15
14
  const buttons = [] as ElementActionButtons.Item[]
16
15
 
17
- const { navigateTo } = props.data
18
- if (navigateTo && enableNavigateTo) {
16
+ const { navigateTo, fqn } = props.data
17
+ if (navigateTo && enableNavigateTo && currentViewId !== navigateTo) {
19
18
  buttons.push({
20
19
  key: 'navigate',
21
20
  icon: <IconZoomScan />,
@@ -25,16 +24,26 @@ export const ElementActions = (props: ElementActionsProps) => {
25
24
  },
26
25
  })
27
26
  }
28
- // if (fqn !== browser.getState().subject) {
29
- // buttons.push({
30
- // key: 'relationships',
31
- // icon: <IconTransform />,
32
- // onClick: (e) => {
33
- // e.stopPropagation()
34
- // browser.navigateTo(fqn)
35
- // },
36
- // })
37
- // }
27
+ if (fqn) {
28
+ buttons.push({
29
+ key: 'relationships',
30
+ icon: <IconTransform />,
31
+ onClick: (e) => {
32
+ e.stopPropagation()
33
+ diagram.openRelationshipsBrowser(fqn)
34
+ },
35
+ })
36
+ }
37
+ if (fqn && enableVscode) {
38
+ buttons.push({
39
+ key: 'goToSource',
40
+ icon: <IconFileSymlink />,
41
+ onClick: (e) => {
42
+ e.stopPropagation()
43
+ diagram.openSource({ element: fqn })
44
+ },
45
+ })
46
+ }
38
47
  return (
39
48
  <ElementActionButtons
40
49
  buttons={buttons}
@@ -31,7 +31,7 @@ export const RelationshipEdge = customEdge<RelationshipDetailsTypes.EdgeData>((p
31
31
  translate: 'translate(-50%, 0)',
32
32
  }}
33
33
  style={{
34
- maxWidth: Math.abs(props.targetX - props.sourceX - 70),
34
+ maxWidth: Math.abs(props.targetX - props.sourceX - 100),
35
35
  }}>
36
36
  <EdgeLabel edgeProps={props}>
37
37
  {enableNavigateTo && navigateTo && (
@@ -1,4 +1,4 @@
1
- import { type EdgeId, nonNullable } from '@likec4/core'
1
+ import { type EdgeId, type Fqn, nonNullable } from '@likec4/core'
2
2
  import { useCallbackRef } from '@mantine/hooks'
3
3
  import { useSelector } from '@xstate/react'
4
4
  import { shallowEqual } from 'fast-equals'
@@ -26,15 +26,17 @@ export function useRelationshipDetails() {
26
26
  const [, startTransition] = useTransition()
27
27
  return useMemo(() => ({
28
28
  actor,
29
+ get rootElementId(): string {
30
+ return `relationship-details-${actor.sessionId.replaceAll(':', '_')}`
31
+ },
29
32
  getState: () => actor.getSnapshot().context,
30
33
  send: actor.send,
31
- navigateTo: (edgeId: EdgeId) => {
32
- startTransition(() => {
33
- actor.send({
34
- type: 'navigate.to',
35
- edgeId,
36
- })
37
- })
34
+ navigateTo: (...params: [edgeId: EdgeId] | [source: Fqn, target: Fqn]) => {
35
+ if (params.length === 1) {
36
+ actor.send({ type: 'navigate.to', params: { edgeId: params[0] } })
37
+ } else {
38
+ actor.send({ type: 'navigate.to', params: { source: params[0], target: params[1] } })
39
+ }
38
40
  },
39
41
  fitDiagram: () => {
40
42
  startTransition(() => {