@eventcatalog/core 5.0.0-beta.11 → 5.0.0-beta.12
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/README.md +1 -1
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-76DAAJLV.js → chunk-BWC4YKW3.js} +1 -1
- package/dist/{chunk-4YFM63DS.js → chunk-CX2VB7AI.js} +1 -1
- package/dist/{chunk-MFEMKQZ6.js → chunk-FY6MSP6J.js} +1 -1
- package/dist/{chunk-LQZ7IG7B.js → chunk-QB7X34OD.js} +1 -1
- package/dist/{chunk-WM4QYMIA.js → chunk-TSY5CC27.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +34 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +67 -67
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
- package/eventcatalog/src/features/studio/agent-choreography.ts +8 -6
- package/eventcatalog/src/features/studio/api/canvases-api.ts +64 -15
- package/eventcatalog/src/features/studio/api/client.ts +6 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +140 -22
- package/eventcatalog/src/features/studio/canvas-doc.ts +165 -10
- package/eventcatalog/src/features/studio/catalog.ts +15 -10
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +54 -89
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +43 -26
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +8 -11
- package/eventcatalog/src/features/studio/components/Comments.tsx +15 -8
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +27 -43
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +32 -4
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +47 -38
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +323 -99
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +33 -17
- package/eventcatalog/src/features/studio/flow-state.ts +29 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/grouping.ts +4 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +1 -1
- package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +20 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +8 -3
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +67 -13
- package/eventcatalog/src/features/studio/layout.ts +29 -15
- package/eventcatalog/src/features/studio/level-layout.ts +145 -0
- package/eventcatalog/src/features/studio/levels.ts +82 -27
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/node-types.ts +44 -4
- package/eventcatalog/src/features/studio/pages/index.astro +2 -1
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
- package/eventcatalog/src/features/studio/server/runtime.ts +12 -4
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/package.json +2 -2
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent as ReactMouseEvent } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
applyNodeChanges,
|
|
4
4
|
ConnectionLineType,
|
|
5
5
|
ReactFlow,
|
|
6
6
|
ReactFlowProvider,
|
|
7
7
|
useReactFlow,
|
|
8
|
+
type Connection,
|
|
8
9
|
type Edge,
|
|
9
10
|
type Node,
|
|
10
11
|
type NodeChange,
|
|
11
12
|
type XYPosition,
|
|
12
13
|
} from '@xyflow/react';
|
|
13
|
-
import { edgeTypes } from '@eventcatalog/visualiser';
|
|
14
|
+
import { DiagramBackground, edgeTypes, getLegend, LegendPanel, pauseEdgeAnimations } from '@eventcatalog/visualiser';
|
|
14
15
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
15
16
|
import { getCatalogConnections, indexCatalog, planWithConnections, type ConnectionGroup } from '../canvas-actions';
|
|
16
|
-
import { isDecided } from '../canvas-doc';
|
|
17
|
+
import { getDiagramLevel, getDiagramLevelUnavailable, isDecided, type LevelPlace } from '../canvas-doc';
|
|
17
18
|
import { CLIPBOARD_TYPE, copyNodes, pasteNodes, readClipboard, type ClipboardContent } from '../clipboard';
|
|
19
|
+
import { keepUnchanged, LOCAL_EDGE_KEYS, LOCAL_NODE_KEYS } from '../flow-state';
|
|
18
20
|
import type { CatalogRelation, CatalogResource } from '../catalog-resources';
|
|
19
21
|
import {
|
|
20
22
|
buildContainer,
|
|
@@ -27,8 +29,10 @@ import {
|
|
|
27
29
|
} from '../catalog';
|
|
28
30
|
import {
|
|
29
31
|
findDropTarget,
|
|
32
|
+
containerFor,
|
|
30
33
|
findGroupAtPoint,
|
|
31
34
|
getAbsolutePosition,
|
|
35
|
+
getAbsoluteRect,
|
|
32
36
|
previewContainerGrowth,
|
|
33
37
|
sizeOf,
|
|
34
38
|
sortByHierarchy,
|
|
@@ -40,7 +44,17 @@ import { useStudioFlow } from '../hooks/use-studio-flow';
|
|
|
40
44
|
import { useComments, type CommentAnchor, type Thread } from '../hooks/use-comments';
|
|
41
45
|
import { getLayoutPositions, layoutGraph } from '../layout';
|
|
42
46
|
import { getLevelGraph, getLevelUnavailableReason, LEVELS, type Level } from '../levels';
|
|
43
|
-
import {
|
|
47
|
+
import { arrangeLevel, getLevelNodeSize, levelPlaces, withDiagramRoutes } from '../level-layout';
|
|
48
|
+
import {
|
|
49
|
+
canGoInContainer,
|
|
50
|
+
getNodeDefinition,
|
|
51
|
+
getNodeLabel,
|
|
52
|
+
getNodeName,
|
|
53
|
+
getNoteLevel,
|
|
54
|
+
isOnLevel,
|
|
55
|
+
isShownOnLevel,
|
|
56
|
+
isWrittenOnCanvas,
|
|
57
|
+
} from '../node-types';
|
|
44
58
|
import type { ToolSync } from '../tool-sync';
|
|
45
59
|
import CanvasControls, { FIT_VIEW_OPTIONS } from './CanvasControls';
|
|
46
60
|
import CanvasHeader from './CanvasHeader';
|
|
@@ -96,8 +110,30 @@ export type SelectedNode = { id: string; name: string; type: string; catalogReso
|
|
|
96
110
|
|
|
97
111
|
/** How long a level (L1, L2) waits for changes to settle before it's laid out again */
|
|
98
112
|
const LEVEL_RELAYOUT_MS = 300;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Where something added goes. While L1 or L2 is shown it's added to the canvas people edit (L3), below what's on it
|
|
116
|
+
* or in the container it's dropped in, and kept on the level where it's dropped.
|
|
117
|
+
*/
|
|
118
|
+
/** Where a node of this size goes to be centred on a point */
|
|
119
|
+
const centredOn = (center: XYPosition, size: { width: number; height: number }): LevelPlace => ({
|
|
120
|
+
x: center.x - size.width / 2,
|
|
121
|
+
y: center.y - size.height / 2,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
type Landing = {
|
|
125
|
+
/** Where it was dropped (or added), in what's shown */
|
|
126
|
+
point: XYPosition;
|
|
127
|
+
/** Where it's centred on the canvas, and the container it goes in there (else the one at that point, if any) */
|
|
128
|
+
center: XYPosition;
|
|
129
|
+
inside?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Where it's centred on the level it was dropped on (in the container's coordinates when it's in one), and the
|
|
132
|
+
* size it's drawn at there before it's measured
|
|
133
|
+
*/
|
|
134
|
+
onLevel?: { level: 1 | 2; center: XYPosition; size: { width: number; height: number } };
|
|
135
|
+
};
|
|
99
136
|
const DELETE_KEYS = ['Backspace', 'Delete'];
|
|
100
|
-
const LEVEL_FIT_VIEW_OPTIONS = { ...FIT_VIEW_OPTIONS, maxZoom: 1 };
|
|
101
137
|
const NO_THREADS: ReadonlySet<string> = new Set();
|
|
102
138
|
// React Flow's warnings, except that a handle has no node: the left panel's drag previews draw nodes off the canvas
|
|
103
139
|
const onFlowError = (code: string, message: string) => {
|
|
@@ -238,6 +274,13 @@ function Canvas({
|
|
|
238
274
|
const edgesRef = useRef(flow.edges);
|
|
239
275
|
edgesRef.current = flow.edges;
|
|
240
276
|
const lookupNodes = () => new Map(nodesRef.current.map((node) => [node.id, node]));
|
|
277
|
+
/** Selects just these nodes, changing only those whose selection changes (each change copies its node) */
|
|
278
|
+
const select = (ids: ReadonlySet<string>) => {
|
|
279
|
+
const changes: NodeChange[] = nodesRef.current.flatMap((node) =>
|
|
280
|
+
!!node.selected === ids.has(node.id) ? [] : [{ type: 'select' as const, id: node.id, selected: ids.has(node.id) }]
|
|
281
|
+
);
|
|
282
|
+
if (changes.length) flow.onNodesChange(changes);
|
|
283
|
+
};
|
|
241
284
|
|
|
242
285
|
// Agents in the browser (WebMCP) work on this canvas through the page, as the user's agent. Browsers without
|
|
243
286
|
// WebMCP of their own get it through the polyfill once someone turns it on (Connect agent).
|
|
@@ -254,8 +297,7 @@ function Canvas({
|
|
|
254
297
|
resources,
|
|
255
298
|
catalog,
|
|
256
299
|
agent: { name: `${name}'s agent`, color, agent: true },
|
|
257
|
-
select: (ids) =>
|
|
258
|
-
flow.onNodesChange(nodesRef.current.map((node) => ({ type: 'select', id: node.id, selected: ids.includes(node.id) }))),
|
|
300
|
+
select: (ids) => select(new Set(ids)),
|
|
259
301
|
fitView: (ids) => void fitView({ nodes: ids?.map((id) => ({ id })), duration: 400, padding: 0.3, maxZoom: 1.2 }),
|
|
260
302
|
reorder: () => reorder(),
|
|
261
303
|
});
|
|
@@ -279,27 +321,35 @@ function Canvas({
|
|
|
279
321
|
// ---- Adding things ----
|
|
280
322
|
|
|
281
323
|
// A domain or system dropped on the canvas: asked whether it's a card or a container first
|
|
282
|
-
const [chooser, setChooser] = useState<{ resource: CatalogResource;
|
|
324
|
+
const [chooser, setChooser] = useState<{ resource: CatalogResource; at: Landing } | null>(null);
|
|
283
325
|
// A service or message that connects to things not on the canvas yet: asked whether to bring them too
|
|
284
326
|
const [connectionsChooser, setConnectionsChooser] = useState<{
|
|
285
327
|
resource: CatalogResource;
|
|
286
|
-
|
|
328
|
+
at: Landing;
|
|
287
329
|
groups: ConnectionGroup[];
|
|
288
330
|
} | null>(null);
|
|
289
331
|
|
|
290
332
|
// A catalog resource is added with edges to the related resources already on the canvas
|
|
291
|
-
const addResource =
|
|
333
|
+
const addResource = (key: string, point: XYPosition) => {
|
|
292
334
|
const resource = catalog.resourcesByKey.get(key);
|
|
293
335
|
if (!resource) return;
|
|
294
|
-
|
|
336
|
+
const at = landingAt(point, resource.node.type);
|
|
337
|
+
if (canBeContainer(resource)) return setChooser({ resource, at });
|
|
295
338
|
const groups = getCatalogConnections(resource, catalog, keysOnCanvas);
|
|
296
|
-
if (groups.length > 0) return setConnectionsChooser({ resource,
|
|
297
|
-
addCard(resource,
|
|
298
|
-
}
|
|
339
|
+
if (groups.length > 0) return setConnectionsChooser({ resource, at, groups });
|
|
340
|
+
addCard(resource, at);
|
|
341
|
+
};
|
|
299
342
|
|
|
300
|
-
const addCard = (resource: CatalogResource,
|
|
301
|
-
|
|
302
|
-
|
|
343
|
+
const addCard = (resource: CatalogResource, at: Landing) =>
|
|
344
|
+
keepOnLevel(
|
|
345
|
+
at,
|
|
346
|
+
flow.insertNode(
|
|
347
|
+
resource.node.type,
|
|
348
|
+
catalogNodeData(resource),
|
|
349
|
+
at.center,
|
|
350
|
+
(id, existing) => getRelatedEdges(resource, id, existing, catalog.relationsByKey),
|
|
351
|
+
at.inside
|
|
352
|
+
)
|
|
303
353
|
);
|
|
304
354
|
|
|
305
355
|
// Nodes waiting to be shown before the canvas is laid out around them
|
|
@@ -315,38 +365,42 @@ function Canvas({
|
|
|
315
365
|
}, [flow.nodes]);
|
|
316
366
|
|
|
317
367
|
/** A domain or system as a container, with everything it holds in the catalog inside if asked */
|
|
318
|
-
const addContainer = async (resource: CatalogResource,
|
|
368
|
+
const addContainer = async (resource: CatalogResource, at: Landing, withContents: boolean) => {
|
|
319
369
|
const existing = nodesRef.current;
|
|
320
370
|
const built = withContents
|
|
321
|
-
? await buildContainerWithContents(resource, center, catalog, existing)
|
|
322
|
-
: { nodes: [buildContainer(resource, center)], edges: [] };
|
|
371
|
+
? await buildContainerWithContents(resource, at.center, catalog, existing)
|
|
372
|
+
: { nodes: [buildContainer(resource, at.center)], edges: [] };
|
|
323
373
|
// Dropped in another container (e.g. a system in its domain): it goes in it
|
|
324
|
-
const outer =
|
|
374
|
+
const outer = containerFor(at.center, existing, at.inside);
|
|
325
375
|
const nodes = outer
|
|
326
376
|
? built.nodes.map((node) =>
|
|
327
377
|
node.parentId ? node : { ...node, parentId: outer.id, position: toRelativePosition(node.position, outer.id, existing) }
|
|
328
378
|
)
|
|
329
379
|
: built.nodes;
|
|
330
|
-
// With its contents, the canvas is laid out again to make room for it all
|
|
331
|
-
|
|
380
|
+
// With its contents, the canvas is laid out again to make room for it all (not when added on a level: it's
|
|
381
|
+
// below everything on the canvas, and nothing on the level moves)
|
|
382
|
+
if (withContents && existing.length > 0 && !at.onLevel) reorderOnceShown.current = nodes.map((node) => node.id);
|
|
332
383
|
flow.insertNodes(nodes, built.edges);
|
|
384
|
+
keepOnLevel(at, built.nodes.find((node) => !node.parentId)?.id);
|
|
333
385
|
};
|
|
334
386
|
|
|
335
387
|
const choose = useStableCallback((choice: ContainerChoice) => {
|
|
336
388
|
if (!chooser) return;
|
|
337
|
-
if (choice.as === 'card') addCard(chooser.resource, chooser.
|
|
338
|
-
else void addContainer(chooser.resource, chooser.
|
|
389
|
+
if (choice.as === 'card') addCard(chooser.resource, chooser.at);
|
|
390
|
+
else void addContainer(chooser.resource, chooser.at, choice.withContents);
|
|
339
391
|
setChooser(null);
|
|
340
392
|
});
|
|
341
393
|
const cancelChooser = useCallback(() => setChooser(null), []);
|
|
342
394
|
|
|
343
395
|
const chooseConnections = useStableCallback((choice: ConnectionsChoice) => {
|
|
344
396
|
if (!connectionsChooser) return;
|
|
345
|
-
const { resource,
|
|
346
|
-
if (choice.as === 'card') addCard(resource,
|
|
397
|
+
const { resource, at } = connectionsChooser;
|
|
398
|
+
if (choice.as === 'card') addCard(resource, at);
|
|
347
399
|
else {
|
|
348
|
-
const { nodes, edges } = planWithConnections(nodesRef.current, resource, center, choice.include, catalog);
|
|
400
|
+
const { nodes, edges } = planWithConnections(nodesRef.current, resource, at.center, choice.include, catalog, at.inside);
|
|
349
401
|
flow.insertNodes(nodes, edges);
|
|
402
|
+
// What it connects to is placed next to it on the level
|
|
403
|
+
keepOnLevel(at, nodes.find((node) => getCatalogLink(node)?.key === resource.key)?.id);
|
|
350
404
|
}
|
|
351
405
|
setConnectionsChooser(null);
|
|
352
406
|
});
|
|
@@ -370,25 +424,55 @@ function Canvas({
|
|
|
370
424
|
flow.insertNode('note', levelRef.current === 3 ? data : { ...data, level: levelRef.current }, center);
|
|
371
425
|
};
|
|
372
426
|
/**
|
|
373
|
-
* Where something added
|
|
374
|
-
*
|
|
427
|
+
* Where something added at a point goes (see Landing). On L1 and L2 it's centred where it's dropped, in the
|
|
428
|
+
* container there (and in the same container on the canvas). Something the level doesn't show (a message on L1)
|
|
429
|
+
* goes to L3, where it's shown.
|
|
375
430
|
*/
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
431
|
+
const landingAt = (point: XYPosition, type: string | undefined): Landing => {
|
|
432
|
+
const level = levelRef.current;
|
|
433
|
+
if (level === 3) return { point, center: point };
|
|
434
|
+
const shownHere = isShownOnLevel(type, level);
|
|
435
|
+
if (!shownHere) changeLevel(3);
|
|
436
|
+
// The container it's dropped in on the level, which the canvas has too (levels keep the canvas's ids)
|
|
437
|
+
const shown = shownHere && levelGraphRef.current?.level === level ? levelGraphRef.current.nodes : [];
|
|
438
|
+
const container = canGoInContainer(type) ? findGroupAtPoint(point, shown) : undefined;
|
|
439
|
+
const canvas = nodesRef.current;
|
|
440
|
+
const inside = container && containerFor(point, canvas, container.id) ? container.id : undefined;
|
|
441
|
+
// On the canvas: below what's in that container, or below everything
|
|
442
|
+
const lookup = new Map(canvas.map((node) => [node.id, node]));
|
|
443
|
+
const beside = canvas.filter((node) => (inside ? node.parentId === inside : !node.parentId && isOnLevel(node, 3)));
|
|
444
|
+
const rects = (beside.length ? beside : inside ? [lookup.get(inside)!] : []).map((node) => getAbsoluteRect(node, lookup));
|
|
445
|
+
const center = rects.length
|
|
446
|
+
? {
|
|
447
|
+
x: (Math.min(...rects.map((rect) => rect.x)) + Math.max(...rects.map((rect) => rect.x + rect.width))) / 2,
|
|
448
|
+
// Below them, or in the middle of an empty container
|
|
449
|
+
y: beside.length ? Math.max(...rects.map((rect) => rect.y + rect.height)) + 160 : rects[0].y + rects[0].height / 2,
|
|
450
|
+
}
|
|
451
|
+
: { x: 0, y: 0 };
|
|
452
|
+
if (!shownHere) return { point, center };
|
|
453
|
+
const origin =
|
|
454
|
+
container && inside ? getAbsolutePosition(container, new Map(shown.map((node) => [node.id, node]))) : { x: 0, y: 0 };
|
|
455
|
+
const size = getLevelNodeSize(type, level);
|
|
456
|
+
return { point, center, inside, onLevel: { level, center: { x: point.x - origin.x, y: point.y - origin.y }, size } };
|
|
457
|
+
};
|
|
458
|
+
// Added on a level, until they're measured, with the size they were centred at: centred again at their real size
|
|
459
|
+
const centring = useRef(new Map<string, { level: 1 | 2; size: { width: number; height: number } }>());
|
|
460
|
+
/** Keeps what was added where it was dropped on the level (undone with adding it) */
|
|
461
|
+
const keepOnLevel = (at: Landing, id: string | undefined) => {
|
|
462
|
+
if (!id || !at.onLevel) return;
|
|
463
|
+
const { level, center, size } = at.onLevel;
|
|
464
|
+
centring.current.set(id, { level, size });
|
|
465
|
+
flow.setLevelLayout(level, new Map([[id, centredOn(center, size)]]));
|
|
466
|
+
};
|
|
467
|
+
const addComponentAt = (type: string, point: XYPosition) => {
|
|
468
|
+
if (type === 'note') return addNoteAt(point);
|
|
469
|
+
const at = landingAt(point, type);
|
|
470
|
+
keepOnLevel(at, flow.addNode(type, at.center, at.inside));
|
|
385
471
|
};
|
|
386
|
-
const addComponentAt = (type: string, center: XYPosition) =>
|
|
387
|
-
type === 'note' ? addNoteAt(center) : flow.addNode(type, centerOnCanvas(center));
|
|
388
472
|
|
|
389
473
|
const addComponent = useStableCallback((type: string) => addComponentAt(type, canvasCenter()));
|
|
390
474
|
const addNote = useStableCallback(() => addNoteAt(canvasCenter()));
|
|
391
|
-
const addResourceFromPanel = useStableCallback((key: string) => addResource(key,
|
|
475
|
+
const addResourceFromPanel = useStableCallback((key: string) => addResource(key, canvasCenter()));
|
|
392
476
|
|
|
393
477
|
const onDrop = useStableCallback((event: DragEvent) => {
|
|
394
478
|
event.preventDefault();
|
|
@@ -396,7 +480,7 @@ function Canvas({
|
|
|
396
480
|
const type = event.dataTransfer.getData(COMPONENT_DRAG_TYPE);
|
|
397
481
|
const resourceKey = event.dataTransfer.getData(CATALOG_DRAG_TYPE);
|
|
398
482
|
if (type) addComponentAt(type, position);
|
|
399
|
-
if (resourceKey) addResource(resourceKey,
|
|
483
|
+
if (resourceKey) addResource(resourceKey, position);
|
|
400
484
|
});
|
|
401
485
|
|
|
402
486
|
// ---- Pointer, panning and dragging ----
|
|
@@ -486,8 +570,13 @@ function Canvas({
|
|
|
486
570
|
// While the canvas is panned, zoomed or dragged, message edges stop animating (a class, not state, so nothing re-renders)
|
|
487
571
|
const interactions = useRef({ moving: false, dragging: false });
|
|
488
572
|
const setInteraction = useCallback((kind: 'moving' | 'dragging', on: boolean) => {
|
|
573
|
+
const wasActive = interactions.current.moving || interactions.current.dragging;
|
|
489
574
|
interactions.current[kind] = on;
|
|
490
|
-
|
|
575
|
+
const active = interactions.current.moving || interactions.current.dragging;
|
|
576
|
+
if (active === wasActive) return;
|
|
577
|
+
canvasRef.current?.classList.toggle('ec-interaction-active', active);
|
|
578
|
+
// (hiding them doesn't stop the envelopes' SMIL)
|
|
579
|
+
pauseEdgeAnimations(canvasRef.current, active);
|
|
491
580
|
}, []);
|
|
492
581
|
|
|
493
582
|
// The camera follows agents unless people move it themselves (set by AgentActivity)
|
|
@@ -589,43 +678,91 @@ function Canvas({
|
|
|
589
678
|
|
|
590
679
|
// ---- Levels of detail ----
|
|
591
680
|
|
|
592
|
-
// Like EventCatalog's diagrams: L3 is the canvas
|
|
593
|
-
|
|
681
|
+
// Like EventCatalog's diagrams: L3 is everything on the canvas, L1 and L2 less detailed views of it, arranged and
|
|
682
|
+
// edited on their own (what's done there changes the canvas: a system card is the system). A canvas opened
|
|
683
|
+
// from a diagram opens at the level it was shown at (?level=1 or 2), once: reloading opens the canvas (L3).
|
|
684
|
+
const [level, setLevel] = useState<Level>(() => {
|
|
685
|
+
const url = new URL(window.location.href);
|
|
686
|
+
const asked = Number(url.searchParams.get('level'));
|
|
687
|
+
if (!url.searchParams.has('level')) return 3;
|
|
688
|
+
url.searchParams.delete('level');
|
|
689
|
+
window.history.replaceState(window.history.state, '', url);
|
|
690
|
+
return asked === 1 || asked === 2 ? asked : 3;
|
|
691
|
+
});
|
|
594
692
|
const levelRef = useRef(level);
|
|
595
693
|
levelRef.current = level;
|
|
694
|
+
// L3 is the canvas itself: comments and other people's pointers are there (L1 and L2 are arranged on their own)
|
|
596
695
|
const editable = level === 3;
|
|
597
|
-
const [levelGraph,
|
|
696
|
+
const [levelGraph, setLevelGraphState] = useState<{ level: Level; nodes: Node[]; edges: Edge[] } | null>(null);
|
|
697
|
+
// The level shown as it is now (for changes to it, and saving where things are put)
|
|
698
|
+
const levelGraphRef = useRef(levelGraph);
|
|
699
|
+
/** Changes to the level shown (moved, selected, measured), as they happen */
|
|
700
|
+
const updateLevelGraph = useCallback((next: { level: Level; nodes: Node[]; edges: Edge[] }) => {
|
|
701
|
+
levelGraphRef.current = next;
|
|
702
|
+
setLevelGraphState(next);
|
|
703
|
+
}, []);
|
|
704
|
+
/**
|
|
705
|
+
* Shows a level (arranged again), keeping what hasn't changed on it as it's shown (so it doesn't render again), and
|
|
706
|
+
* what's selected and nodes' measured sizes (not measured again). Containers come before what's in them, as React
|
|
707
|
+
* Flow needs (the canvas's order can have them after).
|
|
708
|
+
*/
|
|
709
|
+
const setLevelGraph = useCallback((next: { level: Level; nodes: Node[]; edges: Edge[] } | null) => {
|
|
710
|
+
const previous = levelGraphRef.current;
|
|
711
|
+
const same = previous && next && previous.level === next.level ? previous : null;
|
|
712
|
+
const shown = next && {
|
|
713
|
+
...next,
|
|
714
|
+
nodes: sortByHierarchy(same ? keepUnchanged(same.nodes, next.nodes, LOCAL_NODE_KEYS) : next.nodes),
|
|
715
|
+
edges: same ? keepUnchanged(same.edges, next.edges, LOCAL_EDGE_KEYS) : next.edges,
|
|
716
|
+
};
|
|
717
|
+
levelGraphRef.current = shown;
|
|
718
|
+
setLevelGraphState(shown);
|
|
719
|
+
}, []);
|
|
598
720
|
|
|
599
721
|
// What a level shows depends on what's on the canvas and how it's connected, and names (not positions). Notes
|
|
600
722
|
// aren't laid out with it: adding, moving or writing one doesn't lay the level out again.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
? ''
|
|
604
|
-
: `${flow.nodes
|
|
605
|
-
.filter((node) => node.type !== 'note')
|
|
606
|
-
.map((node) => `${node.id}:${node.type}:${node.parentId ?? ''}:${getNodeName(node.type, node.data)}`)
|
|
607
|
-
.join('|')}#${flow.edges.map((edge) => `${edge.source}>${edge.target}:${String(edge.label ?? '')}`).join('|')}`;
|
|
723
|
+
// Also whether a canvas opened from a diagram still has the diagram's levels: only worked out when needed
|
|
724
|
+
const levelKey = level === 3 ? '' : flow.structureKey;
|
|
608
725
|
const shownLevelRef = useRef<Level | undefined>(undefined);
|
|
609
726
|
shownLevelRef.current = levelGraph?.level;
|
|
727
|
+
const levelLayout = level === 3 ? undefined : flow.levelLayouts[level];
|
|
610
728
|
useEffect(() => {
|
|
611
729
|
if (level === 3) return setLevelGraph(null);
|
|
612
730
|
let cancelled = false;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
731
|
+
const show = () => {
|
|
732
|
+
// A canvas opened from a diagram shows the diagram's levels as it did, until what's on the canvas changes
|
|
733
|
+
const fromDiagram = getDiagramLevel(flow.diagramLevels, level, flow.structureKey);
|
|
734
|
+
const diagram = flow.diagramLevels?.[level];
|
|
735
|
+
const derived = fromDiagram ? undefined : getLevelGraph(nodesRef.current, edgesRef.current, level);
|
|
736
|
+
const content = fromDiagram ?? { ...derived!, edges: withDiagramRoutes(derived!.edges, diagram?.edges) };
|
|
737
|
+
// Where things are: as arranged on the level, else as the diagram had them, and anything new next to what it
|
|
738
|
+
// connects to (kept there, so nothing moves later)
|
|
739
|
+
const arranged = arrangeLevel(content, flow.levelLayouts[level], diagram && levelPlaces(diagram.nodes));
|
|
740
|
+
if (arranged) {
|
|
741
|
+
setLevelGraph({ level, ...arranged.graph });
|
|
742
|
+
if (arranged.placed.size) flow.setLevelLayout(level, arranged.placed, 'level');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Nothing on it has a place yet: laid out with the visualiser's layout once, and kept
|
|
746
|
+
void layoutGraph(
|
|
747
|
+
content.nodes.map((node) => ({ ...node, selected: false })),
|
|
748
|
+
content.edges
|
|
749
|
+
).then((laidOut) => {
|
|
750
|
+
if (cancelled) return;
|
|
751
|
+
setLevelGraph({ level, ...laidOut });
|
|
752
|
+
flow.setLevelLayout(level, levelPlaces(laidOut.nodes), 'level');
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
// Straight away when switching levels (or from the diagram); after changes settle while one is shown
|
|
756
|
+
if (shownLevelRef.current !== level || getDiagramLevel(flow.diagramLevels, level, flow.structureKey)) {
|
|
757
|
+
show();
|
|
758
|
+
return () => void (cancelled = true);
|
|
759
|
+
}
|
|
760
|
+
const timer = setTimeout(show, LEVEL_RELAYOUT_MS);
|
|
624
761
|
return () => {
|
|
625
762
|
cancelled = true;
|
|
626
763
|
clearTimeout(timer);
|
|
627
764
|
};
|
|
628
|
-
}, [level, levelKey]);
|
|
765
|
+
}, [level, levelKey, flow.diagramLevels, levelLayout]);
|
|
629
766
|
|
|
630
767
|
// Comments: right click > Add comment, or C for comment mode then click (like Figma)
|
|
631
768
|
const [commentMode, setCommentMode] = useState(false);
|
|
@@ -652,19 +789,34 @@ function Canvas({
|
|
|
652
789
|
});
|
|
653
790
|
const editInL3 = useCallback(() => changeLevel(3), [changeLevel]);
|
|
654
791
|
|
|
655
|
-
// Fit the camera to a level once it's laid out (and to the canvas when going back to L3)
|
|
792
|
+
// Fit the camera to a level once it's laid out (and to the canvas when going back to L3). A level shown before the
|
|
793
|
+
// canvas has arrived (opened at ?level=) is laid out empty first: it's fitted once it has something to show.
|
|
656
794
|
const shownLevel = level === 3 ? 3 : levelGraph?.level;
|
|
795
|
+
const levelHasNodes = !!levelGraph?.nodes.length;
|
|
657
796
|
useEffect(() => {
|
|
658
|
-
if (shownLevel === undefined) return;
|
|
659
|
-
|
|
660
|
-
const options = shownLevel === 3 ? FIT_VIEW_OPTIONS : LEVEL_FIT_VIEW_OPTIONS;
|
|
661
|
-
const frame = requestAnimationFrame(() => void fitView({ ...options, duration: 450 }));
|
|
797
|
+
if (shownLevel === undefined || (shownLevel !== 3 && !levelHasNodes)) return;
|
|
798
|
+
const frame = requestAnimationFrame(() => void fitView({ ...FIT_VIEW_OPTIONS, duration: 450 }));
|
|
662
799
|
return () => cancelAnimationFrame(frame);
|
|
663
|
-
}, [shownLevel]);
|
|
800
|
+
}, [shownLevel, levelHasNodes]);
|
|
664
801
|
|
|
665
|
-
/**
|
|
802
|
+
/**
|
|
803
|
+
* Lays the canvas out with the visualiser's layout, for everyone on it (everyone's nodes glide there). On L1 or
|
|
804
|
+
* L2 it lays out just that level (one step to undo), leaving the canvas as it is.
|
|
805
|
+
*/
|
|
666
806
|
const reorder = useStableCallback(async () => {
|
|
667
|
-
|
|
807
|
+
const level = levelRef.current;
|
|
808
|
+
if (level !== 3) {
|
|
809
|
+
const content =
|
|
810
|
+
getDiagramLevel(flow.diagramLevels, level, flow.structureKey) ?? getLevelGraph(nodesRef.current, edgesRef.current, level);
|
|
811
|
+
const laidOut = await layoutGraph(
|
|
812
|
+
content.nodes.map((node) => ({ ...node, selected: false })),
|
|
813
|
+
content.edges
|
|
814
|
+
);
|
|
815
|
+
glide();
|
|
816
|
+
flow.setLevelLayout(level, levelPlaces(laidOut.nodes));
|
|
817
|
+
setTimeout(() => void fitView({ ...FIT_VIEW_OPTIONS, duration: 450 }), 50);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
668
820
|
const positions = await getLayoutPositions(nodesRef.current, edgesRef.current);
|
|
669
821
|
flow.applyPositions(positions);
|
|
670
822
|
setTimeout(() => void fitView({ ...FIT_VIEW_OPTIONS, duration: 450 }), 50);
|
|
@@ -692,27 +844,97 @@ function Canvas({
|
|
|
692
844
|
const notes = sortedNodes.filter((node) => getNoteLevel(node) === levelShown.level);
|
|
693
845
|
return notes.length ? [...levelShown.nodes, ...notes.map(asLevelNote)] : levelShown.nodes;
|
|
694
846
|
}, [levelShown, sortedNodes]);
|
|
695
|
-
|
|
847
|
+
|
|
848
|
+
// The legend, as the visualiser's: how many of each kind of node is shown (not what's written on the canvas), and
|
|
849
|
+
// clicking a kind hides or shows it. Hiding only changes what this person sees.
|
|
850
|
+
const [hiddenLegendKeys, setHiddenLegendKeys] = useState<string[]>([]);
|
|
851
|
+
const toggleLegendKey = useCallback(
|
|
852
|
+
(key: string) => setHiddenLegendKeys((keys) => (keys.includes(key) ? keys.filter((other) => other !== key) : [...keys, key])),
|
|
853
|
+
[]
|
|
854
|
+
);
|
|
855
|
+
// What's shown only changes with what's on it (not as nodes are dragged, on any level)
|
|
856
|
+
const shownStructure = levelShown ? structureOf(levelShown.nodes) : structure;
|
|
857
|
+
const legend = useMemo(() => getLegend(shownNodes.filter((node) => !isWrittenOnCanvas(node.type))), [shownStructure]);
|
|
858
|
+
const visibleNodes = useMemo(
|
|
859
|
+
() =>
|
|
860
|
+
hiddenLegendKeys.length === 0
|
|
861
|
+
? shownNodes
|
|
862
|
+
: shownNodes.map((node) => (hiddenLegendKeys.includes(node.type ?? '') ? { ...node, hidden: true } : node)),
|
|
863
|
+
[shownNodes, hiddenLegendKeys]
|
|
864
|
+
);
|
|
865
|
+
/**
|
|
866
|
+
* Changes on L1 and L2. The level's notes are on the canvas, so changes to them go there; removing something
|
|
867
|
+
* removes it from the canvas (the level is drawn from it); and moving or resizing something arranges the level,
|
|
868
|
+
* kept when it's dropped.
|
|
869
|
+
*/
|
|
696
870
|
const onLevelNodesChange = useStableCallback((changes: NodeChange[]) => {
|
|
697
|
-
const
|
|
698
|
-
const
|
|
871
|
+
const level = levelRef.current;
|
|
872
|
+
const current = levelGraphRef.current;
|
|
873
|
+
if (level === 3 || !current) return;
|
|
874
|
+
const notes = new Set(nodesRef.current.filter((node) => getNoteLevel(node) === level).map((node) => node.id));
|
|
875
|
+
const isNote = (change: NodeChange) => 'id' in change && notes.has(change.id);
|
|
876
|
+
const ofNotes = changes.filter((change) => change.type !== 'add' && isNote(change));
|
|
699
877
|
if (ofNotes.length) flow.onNodesChange(ofNotes);
|
|
878
|
+
|
|
879
|
+
const ofLevel = changes.filter((change) => !isNote(change) && change.type !== 'add');
|
|
880
|
+
const onCanvas = new Set(nodesRef.current.map((node) => node.id));
|
|
881
|
+
const removed = ofLevel.flatMap((change) => (change.type === 'remove' && onCanvas.has(change.id) ? [change.id] : []));
|
|
882
|
+
if (removed.length) flow.deleteNodes(removed);
|
|
883
|
+
const arranging = ofLevel.filter((change) => change.type !== 'remove');
|
|
884
|
+
if (!arranging.length) return;
|
|
885
|
+
const nodes = applyNodeChanges(arranging, current.nodes);
|
|
886
|
+
updateLevelGraph({ ...current, nodes });
|
|
887
|
+
const done = new Set(
|
|
888
|
+
arranging.flatMap((change) =>
|
|
889
|
+
(change.type === 'position' && change.dragging === false) || (change.type === 'dimensions' && change.resizing === false)
|
|
890
|
+
? [change.id]
|
|
891
|
+
: []
|
|
892
|
+
)
|
|
893
|
+
);
|
|
894
|
+
if (done.size) flow.setLevelLayout(level, levelPlaces(nodes.filter((node) => done.has(node.id))));
|
|
895
|
+
// Something just added, now it's measured: centred where it was dropped, from where it is now (a container it's in
|
|
896
|
+
// may have grown around it), in one step to undo with adding it
|
|
897
|
+
const measured = arranging.flatMap((change) => {
|
|
898
|
+
const dropped = change.type === 'dimensions' && change.dimensions && centring.current.get(change.id);
|
|
899
|
+
const node = dropped && nodes.find((candidate) => candidate.id === change.id);
|
|
900
|
+
if (!dropped || !node) return [];
|
|
901
|
+
centring.current.delete(change.id);
|
|
902
|
+
const { width, height } = change.dimensions!;
|
|
903
|
+
const { x, y } = node.position;
|
|
904
|
+
const place = { x: x + (dropped.size.width - width) / 2, y: y + (dropped.size.height - height) / 2 };
|
|
905
|
+
return dropped.level === level ? [[change.id, place] as const] : [];
|
|
906
|
+
});
|
|
907
|
+
if (measured.length) flow.setLevelLayout(level, new Map(measured));
|
|
908
|
+
});
|
|
909
|
+
/** Connecting two things on L1 or L2 connects them on the canvas (e.g. a relationship between two systems) */
|
|
910
|
+
const onLevelConnect = useStableCallback((connection: Connection) => {
|
|
911
|
+
const onCanvas = new Set(nodesRef.current.map((node) => node.id));
|
|
912
|
+
if (onCanvas.has(connection.source) && onCanvas.has(connection.target)) flow.onConnect(connection);
|
|
700
913
|
});
|
|
701
914
|
// Message edges' envelopes only move on what's selected (the edge, or a node at either end): any moving
|
|
702
915
|
// envelope at all costs a style recalc and layout every frame, which an editing canvas can't afford at idle
|
|
916
|
+
// (on L1 and L2, the level's edges: the level itself changes on every frame something on it is dragged)
|
|
917
|
+
const levelEdges = levelShown?.edges;
|
|
703
918
|
const shownEdges = useMemo(() => {
|
|
704
|
-
const edges =
|
|
919
|
+
const edges = levelEdges ?? flow.edges;
|
|
705
920
|
const focused = new Set(selectedKey ? selectedKey.split(',') : []);
|
|
706
921
|
return edges.map((edge) =>
|
|
707
922
|
edge.type === 'animated' && !edge.selected && !focused.has(edge.source) && !focused.has(edge.target)
|
|
708
923
|
? withoutEnvelope(edge)
|
|
709
924
|
: edge
|
|
710
925
|
);
|
|
711
|
-
}, [
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
926
|
+
}, [levelEdges, flow.edges, selectedKey]);
|
|
927
|
+
// A canvas opened from a diagram has the levels the diagram has (until what's on it changes)
|
|
928
|
+
const unavailableLevels = useMemo(() => {
|
|
929
|
+
const reason = (level: 1 | 2) => {
|
|
930
|
+
if (getDiagramLevel(flow.diagramLevels, level, flow.structureKey)) return undefined;
|
|
931
|
+
return (
|
|
932
|
+
getDiagramLevelUnavailable(flow.diagramLevels, level, flow.structureKey) ??
|
|
933
|
+
getLevelUnavailableReason(nodesRef.current, level)
|
|
934
|
+
);
|
|
935
|
+
};
|
|
936
|
+
return { 1: reason(1), 2: reason(2) };
|
|
937
|
+
}, [structure, flow.structureKey, flow.diagramLevels]);
|
|
716
938
|
|
|
717
939
|
// ---- People and agents ----
|
|
718
940
|
|
|
@@ -836,7 +1058,8 @@ function Canvas({
|
|
|
836
1058
|
|
|
837
1059
|
// Double clicking a node edits its details (notes are edited on the canvas, by double clicking them too)
|
|
838
1060
|
const onNodeDoubleClick = useStableCallback((_: ReactMouseEvent, node: Node) => {
|
|
839
|
-
|
|
1061
|
+
// On L1 and L2 too: what's there is on the canvas (a system card is the system)
|
|
1062
|
+
if (!isWrittenOnCanvas(node.type) && nodesRef.current.some((other) => other.id === node.id)) setEditingId(node.id);
|
|
840
1063
|
});
|
|
841
1064
|
// ...and inside a container (not on something in it): its body lets the pointer through, so it's on the canvas
|
|
842
1065
|
const onCanvasDoubleClick = useStableCallback((event: ReactMouseEvent) => {
|
|
@@ -937,8 +1160,7 @@ function Canvas({
|
|
|
937
1160
|
// The node whose details are being edited in the panel: opened by double clicking it, or "Edit details" in its
|
|
938
1161
|
// menu (selecting a node doesn't open it). Notes are edited on the canvas.
|
|
939
1162
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
940
|
-
const editing =
|
|
941
|
-
editable && editingId ? flow.nodes.find((node) => node.id === editingId && !isWrittenOnCanvas(node.type)) : undefined;
|
|
1163
|
+
const editing = editingId ? flow.nodes.find((node) => node.id === editingId && !isWrittenOnCanvas(node.type)) : undefined;
|
|
942
1164
|
// What the panel shows (not the node's position, so dragging it doesn't re-render the panel)
|
|
943
1165
|
const inspected = useMemo(
|
|
944
1166
|
() => (editing ? { id: editing.id, type: editing.type, data: editing.data } : null),
|
|
@@ -956,6 +1178,8 @@ function Canvas({
|
|
|
956
1178
|
<DropTargetContext.Provider value={dropTargetId}>
|
|
957
1179
|
<div className="flex h-full min-h-0 text-[rgb(var(--ec-page-text))]">
|
|
958
1180
|
<LeftPanel
|
|
1181
|
+
level={level}
|
|
1182
|
+
onShowAll={editInL3}
|
|
959
1183
|
resources={resources}
|
|
960
1184
|
keysOnCanvas={keysOnCanvas}
|
|
961
1185
|
onAddComponent={addComponent}
|
|
@@ -976,16 +1200,16 @@ function Canvas({
|
|
|
976
1200
|
onDoubleClick={onCanvasDoubleClick}
|
|
977
1201
|
>
|
|
978
1202
|
<ReactFlow
|
|
979
|
-
nodes={
|
|
1203
|
+
nodes={visibleNodes}
|
|
980
1204
|
edges={shownEdges}
|
|
981
1205
|
nodeTypes={nodeTypes}
|
|
982
1206
|
edgeTypes={edgeTypes}
|
|
983
|
-
nodesDraggable
|
|
984
|
-
nodesConnectable
|
|
985
|
-
elementsSelectable
|
|
1207
|
+
nodesDraggable
|
|
1208
|
+
nodesConnectable
|
|
1209
|
+
elementsSelectable
|
|
986
1210
|
onNodesChange={editable ? flow.onNodesChange : onLevelNodesChange}
|
|
987
1211
|
onEdgesChange={editable ? flow.onEdgesChange : undefined}
|
|
988
|
-
onConnect={flow.onConnect}
|
|
1212
|
+
onConnect={editable ? flow.onConnect : onLevelConnect}
|
|
989
1213
|
// Drag either end of a connection to another node (dropped anywhere else, it stays where it was)
|
|
990
1214
|
edgesReconnectable={editable}
|
|
991
1215
|
onReconnect={editable ? flow.onReconnect : undefined}
|
|
@@ -1008,16 +1232,17 @@ function Canvas({
|
|
|
1008
1232
|
connectionRadius={40}
|
|
1009
1233
|
deleteKeyCode={DELETE_KEYS}
|
|
1010
1234
|
multiSelectionKeyCode={MULTI_SELECTION_KEYS}
|
|
1011
|
-
selectionOnDrag
|
|
1235
|
+
selectionOnDrag
|
|
1012
1236
|
// Read only levels can't select: dragging pans them
|
|
1013
|
-
panOnDrag={
|
|
1237
|
+
panOnDrag={PAN_BUTTONS}
|
|
1014
1238
|
panOnScroll
|
|
1015
1239
|
zoomActivationKeyCode={ZOOM_KEYS}
|
|
1016
1240
|
zoomOnDoubleClick={false}
|
|
1017
1241
|
minZoom={0.1}
|
|
1018
1242
|
maxZoom={2}
|
|
1019
1243
|
>
|
|
1020
|
-
<
|
|
1244
|
+
<DiagramBackground />
|
|
1245
|
+
<LegendPanel legend={legend} hiddenKeys={hiddenLegendKeys} onLegendClick={toggleLegendKey} />
|
|
1021
1246
|
<ViewportSharer onChange={flow.setViewport} />
|
|
1022
1247
|
{/* Pointers, selections and comments are on the canvas people edit (L3) */}
|
|
1023
1248
|
{editable && <RemotePresence store={flow.presence} clientId={flow.clientId} />}
|
|
@@ -1054,11 +1279,10 @@ function Canvas({
|
|
|
1054
1279
|
{!editable && (
|
|
1055
1280
|
<div className="absolute left-1/2 top-16 z-10 flex -translate-x-1/2 items-center gap-3 rounded-full border px-4 py-1.5 text-xs shadow-sm bg-[rgb(var(--ec-accent-subtle))] border-[rgb(var(--ec-accent)/0.3)] text-[rgb(var(--ec-page-text))]">
|
|
1056
1281
|
<span>
|
|
1057
|
-
<span className="font-semibold">L{level}</span> · {LEVELS.find((entry) => entry.level === level)?.description}
|
|
1058
|
-
read only, sticky notes can be added
|
|
1282
|
+
<span className="font-semibold">L{level}</span> · {LEVELS.find((entry) => entry.level === level)?.description}
|
|
1059
1283
|
</span>
|
|
1060
1284
|
<button onClick={editInL3} className="font-semibold text-[rgb(var(--ec-accent))] hover:underline">
|
|
1061
|
-
|
|
1285
|
+
Everything is on L3
|
|
1062
1286
|
</button>
|
|
1063
1287
|
</div>
|
|
1064
1288
|
)}
|
|
@@ -1155,7 +1379,7 @@ function Canvas({
|
|
|
1155
1379
|
<ContainerChooser
|
|
1156
1380
|
resource={chooser.resource}
|
|
1157
1381
|
contents={chooserContents}
|
|
1158
|
-
position={flowToScreenPosition(chooser.
|
|
1382
|
+
position={flowToScreenPosition(chooser.at.point)}
|
|
1159
1383
|
onChoose={choose}
|
|
1160
1384
|
onCancel={cancelChooser}
|
|
1161
1385
|
/>
|
|
@@ -1164,7 +1388,7 @@ function Canvas({
|
|
|
1164
1388
|
<ConnectionsChooser
|
|
1165
1389
|
resource={connectionsChooser.resource}
|
|
1166
1390
|
groups={connectionsChooser.groups}
|
|
1167
|
-
position={flowToScreenPosition(connectionsChooser.
|
|
1391
|
+
position={flowToScreenPosition(connectionsChooser.at.point)}
|
|
1168
1392
|
onChoose={chooseConnections}
|
|
1169
1393
|
onCancel={cancelConnectionsChooser}
|
|
1170
1394
|
/>
|