@eventcatalog/core 5.6.0 → 5.7.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.
Files changed (31) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +5 -5
  5. package/dist/{chunk-7TTNTHHN.js → chunk-22KZ4TJF.js} +10 -10
  6. package/dist/{chunk-T3U27UXC.js → chunk-4CV7VC4K.js} +1 -1
  7. package/dist/{chunk-XBRKEQ66.js → chunk-BE5PTWNC.js} +1 -1
  8. package/dist/{chunk-DEO4QEGO.js → chunk-FMWQ2YYN.js} +1 -1
  9. package/dist/{chunk-ETS4G6RR.js → chunk-MOPKQMQP.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +16 -16
  14. package/dist/federation/federate.js +3 -3
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +20 -7
  20. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +21 -1
  21. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -1
  22. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +73 -73
  23. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +73 -73
  24. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +4 -0
  25. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +74 -8
  26. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +9 -4
  27. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +4 -0
  28. package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +14 -0
  29. package/eventcatalog/src/utils/node-graphs/node-resource.ts +26 -0
  30. package/package.json +7 -7
  31. package/dist/{chunk-NCEZDQSW.js → chunk-QJ7KKN2D.js} +7 -7
@@ -30,6 +30,7 @@ export default memo(function NodeDetails({
30
30
  connections,
31
31
  onOpenNode,
32
32
  containerRef,
33
+ nameEmpty,
33
34
  locked,
34
35
  onChange,
35
36
  onClose,
@@ -43,6 +44,8 @@ export default memo(function NodeDetails({
43
44
  onOpenNode: (id: string) => void;
44
45
  /** The canvas, to put the card next to the node */
45
46
  containerRef: RefObject<HTMLElement | null>;
47
+ /** Its name starts empty, to type it straight in (a node just made: it keeps its name if nothing's typed) */
48
+ nameEmpty?: boolean;
46
49
  locked: boolean;
47
50
  onChange: UpdateNodeData;
48
51
  onClose: () => void;
@@ -112,6 +115,7 @@ export default memo(function NodeDetails({
112
115
  onSaved={showSaved}
113
116
  placeholder={`Name this ${label.toLowerCase()}`}
114
117
  className="ec-editable-title"
118
+ startEmpty={nameEmpty}
115
119
  required
116
120
  />
117
121
  </Field>
@@ -98,6 +98,12 @@ import {
98
98
  type HotspotSeverity,
99
99
  type HotspotSpot,
100
100
  RemeasureHandles,
101
+ ConnectChooser,
102
+ isCardType,
103
+ newCardData,
104
+ notPlaceholder,
105
+ useConnectNew,
106
+ type NewLink,
101
107
  } from '@eventcatalog/visualiser/studio';
102
108
  import { X } from 'lucide-react';
103
109
  import type { CatalogRelation, CatalogResource } from '../catalog-resources';
@@ -625,11 +631,23 @@ function Canvas({
625
631
  centring.current.set(id, { level, size });
626
632
  flow.setLevelLayout(level, new Map([[id, centredOn(center, size)]]));
627
633
  };
628
- /** A component (its type, or its key in the components list: the kinds of data store share a type) */
629
- const addComponentAt = (component: string, point: XYPosition) => {
630
- if (component === 'note') return addNoteAt(point);
634
+ /**
635
+ * A component (its type, or its key in the components list: the kinds of data store share a type), connected to
636
+ * the node a connection was dragged from when there is one
637
+ */
638
+ const addComponentAt = (component: string, point: XYPosition, link?: NewLink) => {
639
+ if (component === 'note') return void addNoteAt(point);
631
640
  const at = landingAt(point, getNodeDefinition(component)?.type ?? component);
632
- keepOnLevel(at, flow.addNode(component, at.center, at.inside));
641
+ const id = flow.addNode(component, at.center, at.inside, link);
642
+ keepOnLevel(at, id);
643
+ return id;
644
+ };
645
+ /** A new domain or system as a card (the visualiser's, as Studio on the web makes them), connected to `link`'s node */
646
+ const addCardAt = (type: 'context-domain' | 'system', point: XYPosition, link: NewLink) => {
647
+ const at = landingAt(point, type);
648
+ const id = flow.insertNode(type, newCardData(type), at.center, undefined, at.inside, link);
649
+ keepOnLevel(at, id);
650
+ return id;
633
651
  };
634
652
 
635
653
  const addComponent = useStableCallback((type: string) => addComponentAt(type, canvasCenter()));
@@ -728,6 +746,21 @@ function Canvas({
728
746
  trackPointer(event);
729
747
  });
730
748
 
749
+ // ---- Connecting to something new: a connection let go of on empty canvas ----
750
+
751
+ // A placeholder where it was let go of, connected to the node it came from, until what it is is chosen beside it
752
+ const connect = useConnectNew({
753
+ canvasRef,
754
+ getNode: (id) => nodesRef.current.find((node) => node.id === id),
755
+ getLevel: () => levelRef.current,
756
+ locked,
757
+ onMake: (option, center, link) => {
758
+ const id = isCardType(option.type) ? addCardAt(option.type, center, link) : addComponentAt(option.key, center, link);
759
+ // Selected, and its details open to name it, once it's on the canvas
760
+ if (id) editOnceShown.current = id;
761
+ },
762
+ });
763
+
731
764
  // While the canvas is panned, zoomed or dragged, message edges stop animating (a class, not state, so nothing re-renders)
732
765
  const interactions = useRef({ moving: false, dragging: false });
733
766
  const setInteraction = useCallback((kind: 'moving' | 'dragging', on: boolean) => {
@@ -972,6 +1005,7 @@ function Canvas({
972
1005
  setTool('select');
973
1006
  setDraft(null);
974
1007
  setOpenThreadId(null);
1008
+ connect.cancel();
975
1009
  // What's selected may not be on the next level (e.g. a note added on this one)
976
1010
  flow.onNodesChange(selectedIds().map((id) => ({ type: 'select', id, selected: false })));
977
1011
  setLevel(next);
@@ -1193,7 +1227,8 @@ function Canvas({
1193
1227
  * removes it from the canvas (the level is drawn from it); and moving or resizing something arranges the level,
1194
1228
  * kept when it's dropped.
1195
1229
  */
1196
- const onLevelNodesChange = useStableCallback((changes: NodeChange[]) => {
1230
+ const onLevelNodesChange = useStableCallback((allChanges: NodeChange[]) => {
1231
+ const changes = allChanges.filter(notPlaceholder);
1197
1232
  const level = levelRef.current;
1198
1233
  const current = levelGraphRef.current;
1199
1234
  if (level === 3 || !current) return;
@@ -1232,6 +1267,11 @@ function Canvas({
1232
1267
  });
1233
1268
  if (measured.length) flow.setLevelLayout(level, new Map(measured));
1234
1269
  });
1270
+ /** Changes on the canvas (L3), apart from the placeholder's */
1271
+ const onCanvasNodesChange = useStableCallback((allChanges: NodeChange[]) => {
1272
+ const changes = allChanges.filter(notPlaceholder);
1273
+ if (changes.length) flow.onNodesChange(changes);
1274
+ });
1235
1275
  /** Connecting two things on L1 or L2 connects them on the canvas (e.g. a relationship between two systems) */
1236
1276
  const onLevelConnect = useStableCallback((connection: Connection) => {
1237
1277
  const onCanvas = new Set(nodesRef.current.map((node) => node.id));
@@ -1254,6 +1294,13 @@ function Canvas({
1254
1294
  : edge
1255
1295
  );
1256
1296
  }, [levelEdges, flow.edges, hovered]);
1297
+ // With the placeholder, while a connection let go of on empty canvas is waiting to be told what it connects to
1298
+ const { placeholder } = connect;
1299
+ const flowNodes = useMemo(
1300
+ () => (placeholder ? [...visibleNodes, placeholder.node] : visibleNodes),
1301
+ [visibleNodes, placeholder]
1302
+ );
1303
+ const flowEdges = useMemo(() => (placeholder ? [...shownEdges, placeholder.edge] : shownEdges), [shownEdges, placeholder]);
1257
1304
  // A canvas opened from a diagram has the levels the diagram has (until what's on it changes)
1258
1305
  const unavailableLevels = useMemo(() => {
1259
1306
  const reason = (level: 1 | 2) => {
@@ -1654,6 +1701,20 @@ function Canvas({
1654
1701
  // The node whose details are being edited in its card: opened with the edit button over it, by double clicking it,
1655
1702
  // or "Edit details" in its menu (selecting a node doesn't open it). Notes are edited on the canvas.
1656
1703
  const [editingId, setEditingId] = useState<string | null>(null);
1704
+ // Made at the end of a connection: selected and its details opened once it's on the canvas (looked for as what's on
1705
+ // it changes, not as things move), its name empty to type it straight in. NodeDetails is keyed by the node, so its
1706
+ // name starts empty when its card opens, and only then (the next card opened, even its own again, doesn't).
1707
+ const editOnceShown = useRef<string | null>(null);
1708
+ const [justMadeId, setJustMadeId] = useState<string | null>(null);
1709
+ useEffect(() => {
1710
+ const id = editOnceShown.current;
1711
+ if (!id || !nodesRef.current.some((node) => node.id === id)) return;
1712
+ editOnceShown.current = null;
1713
+ select(new Set([id]));
1714
+ setEditingId(id);
1715
+ setJustMadeId(id);
1716
+ }, [flow.structureKey]);
1717
+ useEffect(() => setJustMadeId((id) => (id === editingId ? id : null)), [editingId]);
1657
1718
  const editing = editingId ? flow.nodes.find((node) => node.id === editingId && !isWrittenOnCanvas(node.type)) : undefined;
1658
1719
  // What the card shows (not the node's position, so dragging it doesn't re-render the card)
1659
1720
  const inspected = useMemo(
@@ -1706,8 +1767,8 @@ function Canvas({
1706
1767
  onDoubleClick={onCanvasDoubleClick}
1707
1768
  >
1708
1769
  <ReactFlow
1709
- nodes={visibleNodes}
1710
- edges={shownEdges}
1770
+ nodes={flowNodes}
1771
+ edges={flowEdges}
1711
1772
  onNodeMouseEnter={onNodeMouseEnter}
1712
1773
  onNodeMouseLeave={onHoverEnd}
1713
1774
  onEdgeMouseEnter={onEdgeMouseEnter}
@@ -1717,12 +1778,15 @@ function Canvas({
1717
1778
  nodesDraggable={!locked && tool !== 'hand'}
1718
1779
  nodesConnectable={!locked && tool !== 'hand'}
1719
1780
  elementsSelectable={tool !== 'hand'}
1720
- onNodesChange={editable ? flow.onNodesChange : onLevelNodesChange}
1781
+ onNodesChange={editable ? onCanvasNodesChange : onLevelNodesChange}
1721
1782
  onEdgesChange={editable ? flow.onEdgesChange : undefined}
1722
1783
  onConnect={editable ? flow.onConnect : onLevelConnect}
1784
+ onConnectEnd={connect.onConnectEnd}
1723
1785
  // Drag either end of a connection to another node (dropped anywhere else, it stays where it was)
1724
1786
  edgesReconnectable={editable && !locked}
1725
1787
  onReconnect={editable ? flow.onReconnect : undefined}
1788
+ onReconnectStart={connect.onReconnectStart}
1789
+ onReconnectEnd={connect.onReconnectEnd}
1726
1790
  reconnectRadius={RECONNECT_RADIUS}
1727
1791
  onDragOver={locked ? undefined : onDragOver}
1728
1792
  onMoveStart={onMoveStart}
@@ -2016,6 +2080,7 @@ function Canvas({
2016
2080
  connections={inspectedConnections}
2017
2081
  onOpenNode={openConnected}
2018
2082
  containerRef={canvasRef}
2083
+ nameEmpty={inspected.id === justMadeId}
2019
2084
  locked={locked}
2020
2085
  onChange={flow.updateNodeData}
2021
2086
  onClose={closePanel}
@@ -2023,6 +2088,7 @@ function Canvas({
2023
2088
  )}
2024
2089
  </div>
2025
2090
 
2091
+ {connect.chooser && <ConnectChooser {...connect.chooser} />}
2026
2092
  {chooser && (
2027
2093
  <ContainerChooser
2028
2094
  kind={chooser.resource.collection === 'domains' ? 'domain' : 'system'}
@@ -55,6 +55,8 @@ import {
55
55
  canGoInContainer,
56
56
  getNodeDefinition,
57
57
  isGroupType,
58
+ linkConnection,
59
+ type NewLink,
58
60
  createHotspot as createHotspotInDoc,
59
61
  updateHotspot as updateHotspotInDoc,
60
62
  deleteHotspot as deleteHotspotInDoc,
@@ -599,7 +601,8 @@ export function useStudioFlow({
599
601
 
600
602
  /**
601
603
  * Add a node centred on a canvas position. `connect` gets the new node's id and the nodes already
602
- * on the canvas, and returns edges to add with it (e.g. a catalog resource's relationships).
604
+ * on the canvas, and returns edges to add with it (e.g. a catalog resource's relationships). `link` connects it to
605
+ * the node a connection was dragged from (in the same change, so one undo takes both back).
603
606
  */
604
607
  const insertNode = useCallback(
605
608
  (
@@ -607,7 +610,8 @@ export function useStudioFlow({
607
610
  data: Record<string, unknown>,
608
611
  center: XYPosition,
609
612
  connect?: (id: string, existing: Node[]) => Edge[],
610
- inside?: string
613
+ inside?: string,
614
+ link?: NewLink
611
615
  ) => {
612
616
  const built = buildNode(type, data, center);
613
617
  withDoc((doc) => {
@@ -618,6 +622,7 @@ export function useStudioFlow({
618
622
  ? { ...built, parentId: group.id, position: toRelativePosition(built.position, group.id, existing) }
619
623
  : built;
620
624
  addNodes(doc, [node], connect?.(node.id, existing) ?? []);
625
+ if (link) connectNodes(doc, linkConnection(node.id, link));
621
626
  if (group) fitGroupToChildren(doc, group.id, renderedSizes());
622
627
  });
623
628
  return built.id;
@@ -627,9 +632,9 @@ export function useStudioFlow({
627
632
 
628
633
  /** Add a node of a component (its type, or its key in the components list), centred on a canvas position (in a container, if asked) */
629
634
  const addNode = useCallback(
630
- (component: string, center: XYPosition, inside?: string) => {
635
+ (component: string, center: XYPosition, inside?: string, link?: NewLink) => {
631
636
  const definition = getNodeDefinition(component);
632
- return definition ? insertNode(definition.type, definition.createData(), center, undefined, inside) : undefined;
637
+ return definition ? insertNode(definition.type, definition.createData(), center, undefined, inside, link) : undefined;
633
638
  },
634
639
  [insertNode]
635
640
  );
@@ -75,6 +75,7 @@ import {
75
75
  isArchitectureGraphEnabled,
76
76
  } from '@utils/feature';
77
77
  import { getMDXComponentsByName, parseMdxBooleanProp } from '@utils/markdown';
78
+ import { isFlowGroupBy } from '@eventcatalog/visualiser';
78
79
 
79
80
  import type { CollectionTypes } from '@types';
80
81
 
@@ -349,6 +350,8 @@ let nodeGraphs =
349
350
  ...graph,
350
351
  search: parseMdxBooleanProp(nodeGraph.search, true),
351
352
  legend: parseMdxBooleanProp(nodeGraph.legend, true),
353
+ // Flows only: group the steps to start with (a string in MDX, so only a way they can be grouped)
354
+ lanes: isFlowGroupBy(nodeGraph.lanes) ? nodeGraph.lanes : undefined,
352
355
  };
353
356
  }) || [];
354
357
  const flowEmbeds = getMDXComponentsByName(props.body || '', 'Flow') || [];
@@ -649,6 +652,7 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
649
652
  linksToVisualiser={true}
650
653
  showSearch={nodeGraph.search ?? true}
651
654
  showLegend={nodeGraph.legend ?? true}
655
+ lanes={collection === 'flows' ? nodeGraph.lanes : undefined}
652
656
  href={
653
657
  isVisualiserEnabled()
654
658
  ? {
@@ -37,6 +37,8 @@ const RESOURCE_DATA_KEYS = [
37
37
  // An entity's identifier, its primary key in the Mermaid export
38
38
  'identifier',
39
39
  'sidebar',
40
+ // `visualiser: false`: it has no diagram page, so a node's card doesn't link to one
41
+ 'visualiser',
40
42
  ] as const;
41
43
 
42
44
  const RESOURCE_OBJECT_KEYS = new Set([
@@ -138,6 +140,18 @@ const compactVisualiserResourceData = (value: unknown): unknown => {
138
140
  out.services = value.services.map(compactDomainService);
139
141
  }
140
142
 
143
+ // A service's messages, by id only: a node's card uses them to tell which of a shared channel's messages it sends
144
+ // or receives (pointers, or hydrated messages)
145
+ for (const key of ['sends', 'receives'] as const) {
146
+ const list = value[key];
147
+ if (!Array.isArray(list)) continue;
148
+ out[key] = list.flatMap((pointer) => {
149
+ if (!isPlainObject(pointer)) return [];
150
+ const id = isPlainObject(pointer.data) ? pointer.data.id : pointer.id;
151
+ return id === undefined ? [] : [{ id }];
152
+ });
153
+ }
154
+
141
155
  return out;
142
156
  };
143
157
 
@@ -65,3 +65,29 @@ export const getNodeCatalogResource = (node: DiagramNode): { collection: string;
65
65
  if (!collection || typeof details?.id !== 'string') return undefined;
66
66
  return { collection, id: details.id, ...(typeof details.version === 'string' && { version: details.version }) };
67
67
  };
68
+
69
+ // The collections with a diagram page (pages/visualiser/[type], and domains' and systems' own)
70
+ const DIAGRAM_COLLECTIONS = new Set([
71
+ 'services',
72
+ 'agents',
73
+ 'events',
74
+ 'commands',
75
+ 'queries',
76
+ 'channels',
77
+ 'flows',
78
+ 'containers',
79
+ 'data-products',
80
+ 'domains',
81
+ 'systems',
82
+ ]);
83
+
84
+ /**
85
+ * The path of the diagram page of the resource a node shows (e.g. `/visualiser/services/OrderService/1.0.0`), if it
86
+ * has one: not for actors, users, entities and the like, or a resource with `visualiser: false`
87
+ */
88
+ export const getNodeDiagramPath = (node: DiagramNode): string | undefined => {
89
+ const resource = getNodeCatalogResource(node);
90
+ if (!resource?.version || !DIAGRAM_COLLECTIONS.has(resource.collection)) return undefined;
91
+ if (getNodeResourceDetails(node)?.visualiser === false) return undefined;
92
+ return `/visualiser/${resource.collection}/${resource.id}/${resource.version}`;
93
+ };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.6.0",
10
+ "version": "5.7.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -75,14 +75,14 @@
75
75
  "@radix-ui/react-dropdown-menu": "^2.1.12",
76
76
  "@radix-ui/react-popover": "^1.1.15",
77
77
  "@radix-ui/react-tooltip": "^1.1.8",
78
- "@scalar/api-reference-react": "^0.9.36",
78
+ "@scalar/api-reference-react": "^0.9.82",
79
79
  "@tailwindcss/typography": "^0.5.16",
80
80
  "@tailwindcss/vite": "^4.3.1",
81
81
  "@tanstack/react-table": "^8.17.3",
82
82
  "@xyflow/react": "^12.12.0",
83
83
  "ai": "^7.0.106",
84
84
  "astro": "7.3.8",
85
- "astro-expressive-code": "^0.44.0",
85
+ "astro-expressive-code": "^0.44.3",
86
86
  "astro-seo": "^1.2.0",
87
87
  "boxen": "^8.0.1",
88
88
  "commander": "^12.1.0",
@@ -123,7 +123,7 @@
123
123
  "react-syntax-highlighter": "^16.1.1",
124
124
  "recast": "^0.23.11",
125
125
  "rehype-autolink-headings": "^7.1.0",
126
- "rehype-expressive-code": "^0.44.0",
126
+ "rehype-expressive-code": "^0.44.3",
127
127
  "rehype-slug": "^6.0.0",
128
128
  "remark-comment": "^1.0.0",
129
129
  "remark-directive": "^3.0.0",
@@ -142,10 +142,10 @@
142
142
  "yjs": "^13.6.33",
143
143
  "zod": "^4.6.6",
144
144
  "@eventcatalog/fitness": "0.1.1",
145
- "@eventcatalog/linter": "1.3.0",
146
- "@eventcatalog/visualiser": "^5.1.2",
147
145
  "@eventcatalog/sdk": "2.32.0",
148
- "@eventcatalog/exporters": "0.1.3"
146
+ "@eventcatalog/visualiser": "^5.2.0",
147
+ "@eventcatalog/linter": "1.3.1",
148
+ "@eventcatalog/exporters": "0.1.4"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@astrojs/check": "^0.9.10",
@@ -1,10 +1,3 @@
1
- import {
2
- createFederationContentCache
3
- } from "./chunk-R7Z5ALYI.js";
4
- import {
5
- getFederationDiagnostics,
6
- resolveFederationRules
7
- } from "./chunk-JJPB6EOZ.js";
8
1
  import {
9
2
  withFederationOutputTransaction
10
3
  } from "./chunk-WRNH5C3U.js";
@@ -14,6 +7,13 @@ import {
14
7
  import {
15
8
  createFederationSourceProvider
16
9
  } from "./chunk-A2RZR3U4.js";
10
+ import {
11
+ createFederationContentCache
12
+ } from "./chunk-R7Z5ALYI.js";
13
+ import {
14
+ getFederationDiagnostics,
15
+ resolveFederationRules
16
+ } from "./chunk-JJPB6EOZ.js";
17
17
  import {
18
18
  getEventCatalogConfigFile
19
19
  } from "./chunk-4G7FSRPN.js";