@likec4/diagram 1.27.1 → 1.27.3

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 (123) hide show
  1. package/bundle/index.js +2152 -6757
  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/edge/EdgeLabelContainer.js +3 -2
  11. package/dist/base/primitives/element/ElementActionButtons.js +8 -11
  12. package/dist/base/primitives/element/ElementNodeContainer.css.js +4 -4
  13. package/dist/base/updateEdges.js +1 -9
  14. package/dist/base/updateNodes.js +1 -12
  15. package/dist/bundle/LikeC4View.js +2 -2
  16. package/dist/bundle/LikeC4View.props.d.ts +7 -7
  17. package/dist/hooks/useCurrentViewId.js +8 -2
  18. package/dist/hooks/useDiagram.d.ts +1522 -685
  19. package/dist/hooks/useDiagram.js +21 -24
  20. package/dist/likec4diagram/DiagramXYFlow.js +6 -6
  21. package/dist/likec4diagram/ui/notation/NotationPanel.css.js +3 -3
  22. package/dist/likec4diagram/ui/notation/NotationPanel.js +10 -4
  23. package/dist/node_modules/.pnpm/mnemonist@0.40.3_patch_hash_5c12abb43fcff6fd81e53021b89075d53249640d165a2556cc1e58d71201a13a/node_modules/mnemonist/set.js +369 -0
  24. package/dist/overlays/Overlays.js +3 -1
  25. package/dist/overlays/element-details/actor.d.ts +2769 -173
  26. package/dist/overlays/overlay/Overlay.css.d.ts +26 -2
  27. package/dist/overlays/overlay/Overlay.css.js +66 -18
  28. package/dist/overlays/overlay/Overlay.d.ts +7 -6
  29. package/dist/overlays/overlay/Overlay.js +12 -10
  30. package/dist/overlays/overlaysActor.js +2 -2
  31. package/dist/overlays/relationship-details/RelationshipDetails.js +113 -70
  32. package/dist/overlays/relationship-details/SelectEdge.d.ts +1 -1
  33. package/dist/overlays/relationship-details/SelectEdge.js +15 -9
  34. package/dist/overlays/relationship-details/_types.d.ts +4 -1
  35. package/dist/overlays/relationship-details/actor.d.ts +425 -128
  36. package/dist/overlays/relationship-details/actor.js +157 -53
  37. package/dist/overlays/relationship-details/compute.d.ts +12 -0
  38. package/dist/overlays/relationship-details/compute.js +139 -0
  39. package/dist/overlays/relationship-details/custom/ElementActions.js +27 -5
  40. package/dist/overlays/relationship-details/custom/edgeTypes.js +1 -1
  41. package/dist/overlays/relationship-details/custom/nodeTypes.d.ts +2 -1
  42. package/dist/overlays/relationship-details/hooks.d.ts +336 -6
  43. package/dist/overlays/relationship-details/hooks.js +11 -12
  44. package/dist/overlays/relationship-details/layout-to-xyflow.d.ts +12 -0
  45. package/dist/overlays/relationship-details/layout-to-xyflow.js +145 -0
  46. package/dist/overlays/relationship-details/layout.d.ts +22 -0
  47. package/dist/overlays/relationship-details/layout.js +314 -0
  48. package/dist/overlays/relationships-browser/RelationshipsBrowser.d.ts +3 -1
  49. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +2 -2
  50. package/dist/overlays/relationships-browser/_types.d.ts +2 -0
  51. package/dist/overlays/relationships-browser/actor.d.ts +130 -14
  52. package/dist/overlays/relationships-browser/actor.js +30 -16
  53. package/dist/overlays/relationships-browser/custom/ElementActions.js +5 -3
  54. package/dist/overlays/relationships-browser/custom/edgeTypes.d.ts +3 -1
  55. package/dist/overlays/relationships-browser/custom/edgeTypes.js +1 -1
  56. package/dist/overlays/relationships-browser/hooks.d.ts +3 -1
  57. package/dist/overlays/relationships-browser/hooks.js +6 -9
  58. package/dist/overlays/relationships-browser/{-useRelationshipsView.d.ts → layout.d.ts} +3 -1
  59. package/dist/overlays/relationships-browser/{-useRelationshipsView.js → layout.js} +21 -8
  60. package/dist/overlays/relationships-browser/useViewToNodesEdges.d.ts +1 -1
  61. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +3 -1
  62. package/dist/packages/core/src/compute-view/relationships-view/compute.js +1 -1
  63. package/dist/packages/core/src/model/connection/model/ConnectionModel.js +121 -0
  64. package/dist/packages/core/src/model/connection/model/find.js +39 -0
  65. package/dist/packages/core/src/model/connection/ops.js +7 -0
  66. package/dist/packages/core/src/utils/const.js +3 -0
  67. package/dist/packages/core/src/utils/fqn.js +9 -1
  68. package/dist/packages/core/src/utils/iterable/find.js +21 -0
  69. package/dist/packages/core/src/utils/iterable/some.js +21 -0
  70. package/dist/packages/core/src/utils/set.js +48 -0
  71. package/dist/packages/core/src/utils/string-hash.js +13 -0
  72. package/dist/state/diagram-machine.d.ts +1552 -703
  73. package/dist/state/diagram-machine.js +39 -3
  74. package/dist/state/syncManualLayoutActor.d.ts +1 -1
  75. package/dist/styles.css.js +1 -1
  76. package/dist/utils/index.d.ts +0 -1
  77. package/package.json +11 -11
  78. package/src/LikeC4Diagram.props.ts +7 -3
  79. package/src/LikeC4Diagram.tsx +1 -1
  80. package/src/StaticLikeC4Diagram.tsx +1 -1
  81. package/src/base/BaseXYFlow.tsx +2 -1
  82. package/src/base/primitives/compound/CompoundNodeContainer.tsx +0 -2
  83. package/src/base/primitives/edge/EdgeLabelContainer.tsx +7 -5
  84. package/src/base/primitives/element/ElementActionButtons.tsx +10 -11
  85. package/src/base/primitives/element/ElementNodeContainer.css.ts +4 -4
  86. package/src/base/updateEdges.ts +8 -8
  87. package/src/base/updateNodes.ts +11 -11
  88. package/src/bundle/LikeC4View.props.ts +7 -7
  89. package/src/bundle/LikeC4View.tsx +2 -2
  90. package/src/hooks/useCurrentViewId.ts +4 -2
  91. package/src/hooks/useDiagram.ts +22 -24
  92. package/src/likec4diagram/DiagramXYFlow.tsx +6 -6
  93. package/src/likec4diagram/ui/notation/NotationPanel.css.ts +2 -2
  94. package/src/likec4diagram/ui/notation/NotationPanel.tsx +8 -6
  95. package/src/overlays/Overlays.tsx +4 -2
  96. package/src/overlays/overlay/Overlay.css.ts +67 -17
  97. package/src/overlays/overlay/Overlay.tsx +27 -24
  98. package/src/overlays/overlaysActor.ts +1 -1
  99. package/src/overlays/relationship-details/RelationshipDetails.tsx +131 -68
  100. package/src/overlays/relationship-details/SelectEdge.tsx +19 -12
  101. package/src/overlays/relationship-details/_types.ts +3 -1
  102. package/src/overlays/relationship-details/actor.ts +212 -68
  103. package/src/overlays/relationship-details/compute.ts +186 -0
  104. package/src/overlays/relationship-details/custom/ElementActions.tsx +27 -18
  105. package/src/overlays/relationship-details/custom/edgeTypes.tsx +1 -1
  106. package/src/overlays/relationship-details/hooks.ts +12 -13
  107. package/src/overlays/relationship-details/layout-to-xyflow.ts +172 -0
  108. package/src/overlays/relationship-details/layout.ts +470 -0
  109. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +2 -2
  110. package/src/overlays/relationships-browser/_types.ts +2 -0
  111. package/src/overlays/relationships-browser/actor.ts +39 -14
  112. package/src/overlays/relationships-browser/custom/ElementActions.tsx +5 -3
  113. package/src/overlays/relationships-browser/custom/edgeTypes.tsx +1 -1
  114. package/src/overlays/relationships-browser/hooks.ts +7 -10
  115. package/src/overlays/relationships-browser/{-useRelationshipsView.ts → layout.ts} +26 -33
  116. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +3 -1
  117. package/src/state/diagram-machine.ts +51 -6
  118. package/src/utils/index.ts +0 -1
  119. package/dist/overlays/relationship-details/useLayoutedDetails.d.ts +0 -19
  120. package/dist/overlays/relationship-details/useLayoutedDetails.js +0 -267
  121. package/dist/utils/types.d.ts +0 -8
  122. package/src/overlays/relationship-details/useLayoutedDetails.ts +0 -376
  123. package/src/utils/types.ts +0 -6
@@ -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,53 @@ 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
+ raise({ type: 'fitDiagram', duration: 0 }, { id: 'fitDiagram', delay: 50 }),
325
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 75 }),
197
326
  ],
198
327
  },
328
+ 'xyflow.init': {
329
+ actions: 'updateXYFlow',
330
+ },
331
+ 'xyflow.applyNodeChanges': {
332
+ actions: assign({
333
+ xynodes: ({ context, event }) => {
334
+ return applyNodeChanges(event.changes, context.xynodes)
335
+ },
336
+ }),
337
+ },
338
+ 'xyflow.applyEdgeChanges': {
339
+ actions: assign({
340
+ xyedges: ({ context, event }) => {
341
+ return applyEdgeChanges(event.changes, context.xyedges)
342
+ },
343
+ }),
344
+ },
345
+ 'xyflow.paneDblClick': {
346
+ actions: 'xyflow:fitDiagram',
347
+ },
348
+ 'navigate.to': {
349
+ actions: assign({
350
+ subject: ({ event }) => inputToSubject(event.params),
351
+ viewId: ({ context, event }) => event.params.viewId ?? context.viewId,
352
+ }),
353
+ },
199
354
  'close': {
200
355
  target: 'closed',
201
356
  },
202
357
  },
203
358
  exit: assign({
204
- initialized: false,
359
+ xyedges: [],
360
+ xynodes: [],
361
+ initialized: {
362
+ xydata: false,
363
+ xyflow: false,
364
+ },
205
365
  xyflow: null,
366
+ xystore: null,
206
367
  }),
207
368
  },
208
369
  'closed': {
@@ -222,25 +383,8 @@ export const relationshipDetailsLogic = setup({
222
383
  raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 200 }),
223
384
  ],
224
385
  },
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
- }),
386
+ 'xyflow.updateNodeInternals': {
387
+ actions: 'xyflow:updateNodeInternals',
244
388
  },
245
389
  },
246
390
  })
@@ -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 && (