@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.30
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/LICENSE +234 -134
- package/README.md +5 -5
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +160 -77
- package/dist/analytics/log-build.d.cts +2 -2
- package/dist/analytics/log-build.d.ts +2 -2
- package/dist/analytics/log-build.js +7 -5
- package/dist/analytics/studio-usage.cjs +90 -0
- package/dist/analytics/studio-usage.d.cts +5 -0
- package/dist/analytics/studio-usage.d.ts +5 -0
- package/dist/analytics/studio-usage.js +10 -0
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
- package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
- package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
- package/dist/chunk-CA6WCQ2T.js +54 -0
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
- package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
- package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
- package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
- package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +34 -41
- package/dist/eventcatalog-config-file-utils.d.cts +56 -58
- package/dist/eventcatalog-config-file-utils.d.ts +56 -58
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +531 -356
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +63 -25
- package/dist/features.cjs +34 -41
- package/dist/features.js +2 -2
- package/dist/federation/federate.cjs +37 -44
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +37 -42
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +34 -41
- package/dist/resolve-catalog-dependencies.js +2 -2
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +13 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/child-process-signals.cjs +53 -0
- package/dist/utils/child-process-signals.d.cts +19 -0
- package/dist/utils/child-process-signals.d.ts +19 -0
- package/dist/utils/child-process-signals.js +6 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +78 -45
- package/dist/utils/license-status.d.cts +15 -5
- package/dist/utils/license-status.d.ts +15 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +23 -1
- package/eventcatalog/integrations/config-loader.mjs +56 -58
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
- package/eventcatalog/src/components/Search/search-utils.ts +26 -0
- package/eventcatalog/src/components/Seo.astro +5 -1
- package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
- package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
- package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
- package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
- package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
- package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
- package/eventcatalog/src/components/Settings/Row.tsx +7 -4
- package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- package/eventcatalog/src/components/TrialBadge.astro +20 -9
- package/eventcatalog/src/content.config.ts +19 -2
- package/eventcatalog/src/features/auth/error.astro +4 -2
- package/eventcatalog/src/features/auth/login.astro +2 -1
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
- package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +40 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +173 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
- package/eventcatalog/src/features/studio/identity.ts +59 -0
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
- package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
- package/eventcatalog/src/pages/_index.astro +5 -342
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
- package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
- package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/index.astro +4 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
- package/eventcatalog/src/pages/settings/license.astro +9 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
- package/eventcatalog/src/utils/collections/util.ts +8 -4
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +22 -5
- package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
- package/eventcatalog/src/utils/license.ts +1 -1
- package/eventcatalog/src/utils/link-validation.ts +25 -19
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
- package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
- package/eventcatalog/src/utils/page-title.ts +15 -0
- package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
- package/eventcatalog/src/utils/trial.ts +14 -1
- package/package.json +19 -13
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
import { HocuspocusProvider } from '@hocuspocus/provider';
|
|
4
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
5
|
+
import {
|
|
6
|
+
applyEdgeChanges,
|
|
7
|
+
applyNodeChanges,
|
|
8
|
+
type Connection,
|
|
9
|
+
type Edge,
|
|
10
|
+
type EdgeChange,
|
|
11
|
+
type Node,
|
|
12
|
+
type NodeChange,
|
|
13
|
+
type XYPosition,
|
|
14
|
+
} from '@xyflow/react';
|
|
15
|
+
import {
|
|
16
|
+
addNodes,
|
|
17
|
+
buildNode,
|
|
18
|
+
connectNodes,
|
|
19
|
+
deleteEdges,
|
|
20
|
+
deleteNodes as deleteNodesInDoc,
|
|
21
|
+
reconnectEdge,
|
|
22
|
+
fitContainersAround,
|
|
23
|
+
fitGroupToChildren,
|
|
24
|
+
getCanvasMaps,
|
|
25
|
+
moveNode,
|
|
26
|
+
placeNode,
|
|
27
|
+
changesNodesOrEdges,
|
|
28
|
+
getStructureKey,
|
|
29
|
+
getCanvasStatus,
|
|
30
|
+
readMeta,
|
|
31
|
+
reopenIfDecided,
|
|
32
|
+
resizeNode,
|
|
33
|
+
setCanvasStatus,
|
|
34
|
+
setMeta,
|
|
35
|
+
type Author,
|
|
36
|
+
type CanvasStatus,
|
|
37
|
+
type DiagramLevels,
|
|
38
|
+
type LevelPlace,
|
|
39
|
+
readLevelLayout,
|
|
40
|
+
setLevelPlaces,
|
|
41
|
+
type StatusChange,
|
|
42
|
+
setNodeParents,
|
|
43
|
+
type RenderedSizes,
|
|
44
|
+
updateNodeData as updateNodeDataInDoc,
|
|
45
|
+
keepDragged,
|
|
46
|
+
LOCAL_EDGE_KEYS,
|
|
47
|
+
LOCAL_NODE_KEYS,
|
|
48
|
+
patchShared,
|
|
49
|
+
withPositions,
|
|
50
|
+
createSmoother,
|
|
51
|
+
containerFor,
|
|
52
|
+
toRelativePosition,
|
|
53
|
+
type NodePreview,
|
|
54
|
+
type LayoutResult,
|
|
55
|
+
canGoInContainer,
|
|
56
|
+
getNodeDefinition,
|
|
57
|
+
isGroupType,
|
|
58
|
+
createHotspot as createHotspotInDoc,
|
|
59
|
+
updateHotspot as updateHotspotInDoc,
|
|
60
|
+
deleteHotspot as deleteHotspotInDoc,
|
|
61
|
+
readHotspots,
|
|
62
|
+
getLock,
|
|
63
|
+
setLock,
|
|
64
|
+
type Hotspot,
|
|
65
|
+
type Editing,
|
|
66
|
+
addThreads,
|
|
67
|
+
addHotspots,
|
|
68
|
+
type ModelCanvasContent,
|
|
69
|
+
} from '@eventcatalog/visualiser/studio';
|
|
70
|
+
import { CANVAS_DELETED_REASON, canvasDocumentName } from '../document-name';
|
|
71
|
+
import { applyLayout } from '../agent-choreography';
|
|
72
|
+
import { startToolSync, type ToolSync } from '../tool-sync';
|
|
73
|
+
import { createPresenceSender, type PresenceSender } from './presence-sender';
|
|
74
|
+
import { createPresenceStore, type PresenceStore } from './presence-store';
|
|
75
|
+
|
|
76
|
+
export type { Peer } from './presence-store';
|
|
77
|
+
|
|
78
|
+
/** How long the WebSocket gets to connect before a view that can sync through tool calls switches to them */
|
|
79
|
+
const SOCKET_TIMEOUT_MS = 4000;
|
|
80
|
+
/**
|
|
81
|
+
* After someone drops what they were dragging, how long until it goes back to where the document has it, if the
|
|
82
|
+
* drop doesn't arrive (they left mid-drag). The drop normally arrives first, or within a few milliseconds.
|
|
83
|
+
*/
|
|
84
|
+
const DROP_SETTLE_MS = 500;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Changes made here that are already shown here (nodes dragged and resized with React Flow): the document
|
|
88
|
+
* observers skip them, and undo includes them
|
|
89
|
+
*/
|
|
90
|
+
const LOCAL_ORIGIN = Symbol('studio-local');
|
|
91
|
+
/** Changes an agent working through this tab (WebMCP) makes: synced like anyone's, and not in the user's undo */
|
|
92
|
+
export const BROWSER_AGENT_ORIGIN = Symbol('studio-browser-agent');
|
|
93
|
+
/** Status changes: synced, but not undone (undo takes back edits, not decisions about the canvas) */
|
|
94
|
+
const STATUS_ORIGIN = Symbol('studio-status');
|
|
95
|
+
/** A diagram file opened into a new canvas: what it starts with, synced, not undone (nothing to go back to) */
|
|
96
|
+
const OPEN_ORIGIN = Symbol('studio-open');
|
|
97
|
+
/** Where L1 and L2 put what's new on them, kept so it stays there: synced, not undone (nobody did it) */
|
|
98
|
+
const LEVEL_PLACEMENT_ORIGIN = Symbol('studio-level-placement');
|
|
99
|
+
|
|
100
|
+
const NO_LEVEL_LAYOUTS = { 1: new Map(), 2: new Map() } as const;
|
|
101
|
+
/** What someone changing a note or text shows the others as they change it (see `editing` in their presence) */
|
|
102
|
+
const EDITED_FIELDS = ['text', 'fontSize', 'angle'] as const;
|
|
103
|
+
|
|
104
|
+
export type Status = 'connecting' | 'connected' | 'disconnected';
|
|
105
|
+
export type Transport = 'websocket' | 'tools';
|
|
106
|
+
|
|
107
|
+
type Shared = { doc: Y.Doc; awareness: Awareness; undoManager: Y.UndoManager; sender: PresenceSender };
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A React Flow design kept in a Yjs document (one map of nodes, one of edges), synced through Hocuspocus.
|
|
111
|
+
* Presence (who is here, their pointer and what they have selected) goes over Yjs awareness, in a store
|
|
112
|
+
* (see presence-store) rather than React state.
|
|
113
|
+
*
|
|
114
|
+
* Nodes being dragged go to everyone with our presence (with the pointer, about 30 times a second), and only
|
|
115
|
+
* where they're dropped is saved in the document: one change per drag, so the document doesn't grow with every
|
|
116
|
+
* step of every drag, and undo takes back the whole drag. Others show where we're dragging them as we do.
|
|
117
|
+
*/
|
|
118
|
+
const NEW_CANVAS_STATUS = { status: 'draft' as CanvasStatus, history: [] as StatusChange[] };
|
|
119
|
+
|
|
120
|
+
export function useStudioFlow({
|
|
121
|
+
canvasId,
|
|
122
|
+
socketUrl,
|
|
123
|
+
name,
|
|
124
|
+
color,
|
|
125
|
+
picture,
|
|
126
|
+
agent,
|
|
127
|
+
syncViaTools,
|
|
128
|
+
onLayout,
|
|
129
|
+
onContainerResize,
|
|
130
|
+
}: {
|
|
131
|
+
canvasId: string;
|
|
132
|
+
/** e.g. wss://catalog.example.com/_eventcatalog/studio */
|
|
133
|
+
socketUrl: string;
|
|
134
|
+
name: string;
|
|
135
|
+
color: string;
|
|
136
|
+
/** From your sign-in provider, when you're signed in */
|
|
137
|
+
picture?: string;
|
|
138
|
+
/** You're an AI agent here (e.g. a chat's canvas view, which joins as the chat's agent) */
|
|
139
|
+
agent?: boolean;
|
|
140
|
+
/** Sync through MCP tool calls when the WebSocket can't connect (e.g. inside a chat's sandbox) */
|
|
141
|
+
syncViaTools?: ToolSync;
|
|
142
|
+
/** Someone laid the canvas out (us included): called as the new positions arrive, so they can glide there */
|
|
143
|
+
onLayout?: () => void;
|
|
144
|
+
/**
|
|
145
|
+
* A container changed size (it grew to fit what's in it, or someone else resized it): called as it arrives, so
|
|
146
|
+
* it can grow smoothly. Not for our own resizing, which follows the pointer.
|
|
147
|
+
*/
|
|
148
|
+
onContainerResize?: () => void;
|
|
149
|
+
}) {
|
|
150
|
+
const sharedRef = useRef<Shared | null>(null);
|
|
151
|
+
const [nodes, setNodes] = useState<Node[]>([]);
|
|
152
|
+
const [edges, setEdges] = useState<Edge[]>([]);
|
|
153
|
+
const [status, setStatus] = useState<Status>('connecting');
|
|
154
|
+
const [presence, setPresence] = useState<PresenceStore | null>(null);
|
|
155
|
+
const [clientId, setClientId] = useState<number | null>(null);
|
|
156
|
+
// Exposed so other shared state (e.g. comments) can live in the same document
|
|
157
|
+
const [doc, setDoc] = useState<Y.Doc | null>(null);
|
|
158
|
+
const [history, setHistory] = useState({ canUndo: false, canRedo: false });
|
|
159
|
+
const [title, setTitle] = useState<string | undefined>();
|
|
160
|
+
// L1 and L2 of the diagram the canvas was opened from, if it was
|
|
161
|
+
const [diagramLevels, setDiagramLevels] = useState<DiagramLevels | undefined>();
|
|
162
|
+
// What's on the canvas and how it's connected (getStructureKey): worked out when the document's nodes or edges
|
|
163
|
+
// change, which drags don't do (they're in presence until the drop)
|
|
164
|
+
const [structureKey, setStructureKey] = useState('');
|
|
165
|
+
// Where things are on L1 and L2, as people arranged them
|
|
166
|
+
const [levelLayouts, setLevelLayouts] = useState<Record<1 | 2, ReadonlyMap<string, LevelPlace>>>(NO_LEVEL_LAYOUTS);
|
|
167
|
+
const [canvasStatus, setCanvasStatusState] = useState<{ status: CanvasStatus; history: StatusChange[] }>(NEW_CANVAS_STATUS);
|
|
168
|
+
const [transport, setTransport] = useState<Transport>('websocket');
|
|
169
|
+
// Someone deleted the canvas: the server closed it for everyone, and won't open it again
|
|
170
|
+
const [deleted, setDeleted] = useState(false);
|
|
171
|
+
// What's flagged on the canvas (risks, problems, the key parts), on nodes or at places
|
|
172
|
+
const [hotspots, setHotspots] = useState<Hotspot[]>([]);
|
|
173
|
+
// Who locked the canvas (for everyone on it), if it's locked
|
|
174
|
+
const [lockedBy, setLockedBy] = useState<string | null>(null);
|
|
175
|
+
// The server's copy of the canvas has arrived (what's on it is known, e.g. that it's empty)
|
|
176
|
+
const [synced, setSynced] = useState(false);
|
|
177
|
+
const user = useRef<Author>({ name, color });
|
|
178
|
+
user.current = { name, color, ...(picture && { picture }), ...(agent && { agent }) };
|
|
179
|
+
const onLayoutRef = useRef(onLayout);
|
|
180
|
+
onLayoutRef.current = onLayout;
|
|
181
|
+
const onContainerResizeRef = useRef(onContainerResize);
|
|
182
|
+
onContainerResizeRef.current = onContainerResize;
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
// A new connection starts from what the server has. Clear anything left from a previous one,
|
|
186
|
+
// since an empty document never fires an update that would remove it.
|
|
187
|
+
setNodes([]);
|
|
188
|
+
setEdges([]);
|
|
189
|
+
|
|
190
|
+
const doc = new Y.Doc();
|
|
191
|
+
const {
|
|
192
|
+
nodes: yNodes,
|
|
193
|
+
edges: yEdges,
|
|
194
|
+
threads: yThreads,
|
|
195
|
+
meta: yMeta,
|
|
196
|
+
levelLayouts: yLevelLayouts,
|
|
197
|
+
hotspots: yHotspots,
|
|
198
|
+
} = getCanvasMaps(doc);
|
|
199
|
+
const syncLevelLayouts = (event: Y.YMapEvent<LevelPlace>) => {
|
|
200
|
+
const changed = new Set([...event.keysChanged].map((key) => Number(key.split(':')[0]) as 1 | 2));
|
|
201
|
+
setLevelLayouts((current) => {
|
|
202
|
+
const next = { ...current };
|
|
203
|
+
changed.forEach((level) => (next[level] = readLevelLayout(doc, level)));
|
|
204
|
+
return next;
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
yLevelLayouts.observe(syncLevelLayouts);
|
|
208
|
+
setLevelLayouts(NO_LEVEL_LAYOUTS);
|
|
209
|
+
const syncMeta = (event: Y.YMapEvent<unknown>) => {
|
|
210
|
+
const meta = readMeta(doc);
|
|
211
|
+
setTitle(meta.title);
|
|
212
|
+
if (event.keysChanged.has('diagramLevels')) setDiagramLevels(meta.diagramLevels);
|
|
213
|
+
if (event.keysChanged.has('status') || event.keysChanged.has('statusHistory'))
|
|
214
|
+
setCanvasStatusState({ status: getCanvasStatus(meta), history: meta.statusHistory ?? [] });
|
|
215
|
+
if (event.keysChanged.has('layoutAt')) onLayoutRef.current?.();
|
|
216
|
+
if (event.keysChanged.has('lockedBy')) setLockedBy(getLock(doc));
|
|
217
|
+
};
|
|
218
|
+
yMeta.observe(syncMeta);
|
|
219
|
+
setLockedBy(null);
|
|
220
|
+
const syncHotspots = () => setHotspots(readHotspots(doc));
|
|
221
|
+
yHotspots.observe(syncHotspots);
|
|
222
|
+
setHotspots([]);
|
|
223
|
+
setTitle(undefined);
|
|
224
|
+
setStructureKey('');
|
|
225
|
+
setDiagramLevels(undefined);
|
|
226
|
+
setCanvasStatusState(NEW_CANVAS_STATUS);
|
|
227
|
+
// Changing an accepted (or rejected) canvas here makes it a draft again, for everyone. Changes from others arrive
|
|
228
|
+
// already reopened by whoever made them.
|
|
229
|
+
const reopenOnEdit = (transaction: Y.Transaction) => {
|
|
230
|
+
if (!transaction.local || transaction.origin === STATUS_ORIGIN) return;
|
|
231
|
+
if (!changesNodesOrEdges(transaction)) return;
|
|
232
|
+
// Recorded against whoever changed it: this person, or their agent in the browser (WebMCP)
|
|
233
|
+
const by =
|
|
234
|
+
transaction.origin === BROWSER_AGENT_ORIGIN
|
|
235
|
+
? { name: `${user.current.name}'s agent`, color: user.current.color, agent: true }
|
|
236
|
+
: user.current;
|
|
237
|
+
queueMicrotask(() =>
|
|
238
|
+
doc.transact(() => {
|
|
239
|
+
reopenIfDecided(doc, by);
|
|
240
|
+
// The first thing put on a canvas started it (opening a new one to look around doesn't)
|
|
241
|
+
if (!readMeta(doc).createdAt) setMeta(doc, { createdAt: Date.now(), createdBy: by.name });
|
|
242
|
+
}, STATUS_ORIGIN)
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
doc.on('afterTransaction', reopenOnEdit);
|
|
246
|
+
const syncStructure = (transaction: Y.Transaction) => {
|
|
247
|
+
// Drops and resizes here only move and size things (putting something in a container isn't LOCAL_ORIGIN)
|
|
248
|
+
if (transaction.origin !== LOCAL_ORIGIN && changesNodesOrEdges(transaction))
|
|
249
|
+
setStructureKey(getStructureKey(Array.from(yNodes.values()), Array.from(yEdges.values())));
|
|
250
|
+
};
|
|
251
|
+
doc.on('afterTransaction', syncStructure);
|
|
252
|
+
// One presence for whichever connection is used
|
|
253
|
+
const awareness = new Awareness(doc);
|
|
254
|
+
awareness.setLocalStateField('user', user.current);
|
|
255
|
+
const presenceStore = createPresenceStore(awareness);
|
|
256
|
+
setPresence(presenceStore);
|
|
257
|
+
const sender = createPresenceSender(awareness);
|
|
258
|
+
// Let in by the server, not just open: with sign-in on, it refuses a socket without a session after it opens.
|
|
259
|
+
// `letIn` is for this connection; `socketConnected` stays once the socket has worked (no falling back after).
|
|
260
|
+
let socketConnected = false;
|
|
261
|
+
let letIn = false;
|
|
262
|
+
let stopToolSync: (() => void) | undefined;
|
|
263
|
+
let fallback: ReturnType<typeof setTimeout> | undefined;
|
|
264
|
+
// Declared before the provider: it reports its first status while it's being created
|
|
265
|
+
let socketStatus: Status = 'connecting';
|
|
266
|
+
let canvasDeleted = false;
|
|
267
|
+
const onCanvasDeleted = () => {
|
|
268
|
+
canvasDeleted = true;
|
|
269
|
+
clearTimeout(fallback);
|
|
270
|
+
stopToolSync?.();
|
|
271
|
+
provider.disconnect();
|
|
272
|
+
setDeleted(true);
|
|
273
|
+
setStatus('disconnected');
|
|
274
|
+
};
|
|
275
|
+
// Some views can't use the WebSocket (a chat's sandbox may block it, or open it without the person's session):
|
|
276
|
+
// sync through tool calls instead
|
|
277
|
+
const syncThroughTools = () => {
|
|
278
|
+
if (!syncViaTools || stopToolSync) return;
|
|
279
|
+
clearTimeout(fallback);
|
|
280
|
+
setTransport('tools');
|
|
281
|
+
stopToolSync = startToolSync(doc, awareness, { canvasId, sync: syncViaTools, onStatus: setStatus });
|
|
282
|
+
// After the tools take over, so the socket closing doesn't show as offline for a moment
|
|
283
|
+
provider.disconnect();
|
|
284
|
+
};
|
|
285
|
+
const provider = new HocuspocusProvider({
|
|
286
|
+
url: socketUrl,
|
|
287
|
+
name: canvasDocumentName(canvasId),
|
|
288
|
+
document: doc,
|
|
289
|
+
awareness,
|
|
290
|
+
onStatus: ({ status }) => {
|
|
291
|
+
socketStatus = status as Status;
|
|
292
|
+
if (status !== 'connected') letIn = false;
|
|
293
|
+
// Open isn't connected until the server lets us in (onAuthenticated)
|
|
294
|
+
if (!stopToolSync) setStatus(status === 'connected' && !letIn ? 'connecting' : (status as Status));
|
|
295
|
+
},
|
|
296
|
+
onSynced: ({ state }) => setSynced(state),
|
|
297
|
+
onAuthenticated: () => {
|
|
298
|
+
socketConnected = letIn = true;
|
|
299
|
+
if (!stopToolSync) setStatus('connected');
|
|
300
|
+
},
|
|
301
|
+
// Deleted while it was open here
|
|
302
|
+
onClose: ({ event }) => {
|
|
303
|
+
if (event.reason === CANVAS_DELETED_REASON) onCanvasDeleted();
|
|
304
|
+
},
|
|
305
|
+
// Not signed in (or the session ran out): a chat's view syncs through its tools, a page shows it's offline
|
|
306
|
+
// (coming back to the tab tries again, e.g. after signing in again elsewhere). Or the canvas was deleted.
|
|
307
|
+
onAuthenticationFailed: ({ reason }) => {
|
|
308
|
+
if (reason === CANVAS_DELETED_REASON) return onCanvasDeleted();
|
|
309
|
+
if (syncViaTools) return syncThroughTools();
|
|
310
|
+
provider.disconnect();
|
|
311
|
+
setStatus('disconnected');
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
setTransport('websocket');
|
|
315
|
+
setDeleted(false);
|
|
316
|
+
setSynced(false);
|
|
317
|
+
|
|
318
|
+
// Back online, or back on the tab: reconnect now rather than when the next retry is due
|
|
319
|
+
const reconnect = () => {
|
|
320
|
+
if (!stopToolSync && !canvasDeleted && socketStatus === 'disconnected') void provider.connect();
|
|
321
|
+
};
|
|
322
|
+
// Away from the tab: our pointer goes from everyone's canvas
|
|
323
|
+
const onVisibilityChange = () => {
|
|
324
|
+
if (document.visibilityState === 'visible') return reconnect();
|
|
325
|
+
sender.send({ pointer: null });
|
|
326
|
+
sender.flush();
|
|
327
|
+
};
|
|
328
|
+
window.addEventListener('online', reconnect);
|
|
329
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
330
|
+
|
|
331
|
+
// A socket that isn't let in within a few seconds (blocked, or never answered) is given up for tool calls
|
|
332
|
+
if (syncViaTools) fallback = setTimeout(() => !socketConnected && syncThroughTools(), SOCKET_TIMEOUT_MS);
|
|
333
|
+
// Undo only takes back your own changes: other people's arrive with the provider as origin, agents' with theirs
|
|
334
|
+
const undoManager = new Y.UndoManager([yNodes, yEdges, yThreads, yLevelLayouts, yHotspots], {
|
|
335
|
+
captureTimeout: 500,
|
|
336
|
+
trackedOrigins: new Set([null, LOCAL_ORIGIN]),
|
|
337
|
+
});
|
|
338
|
+
const updateHistory = () =>
|
|
339
|
+
setHistory({ canUndo: undoManager.undoStack.length > 0, canRedo: undoManager.redoStack.length > 0 });
|
|
340
|
+
undoManager.on('stack-item-added', updateHistory);
|
|
341
|
+
undoManager.on('stack-item-popped', updateHistory);
|
|
342
|
+
undoManager.on('stack-cleared', updateHistory);
|
|
343
|
+
setHistory({ canUndo: false, canRedo: false });
|
|
344
|
+
sharedRef.current = { doc, awareness, undoManager, sender };
|
|
345
|
+
setClientId(doc.clientID);
|
|
346
|
+
setDoc(doc);
|
|
347
|
+
|
|
348
|
+
// Where others are dragging nodes, from their presence, until they drop them
|
|
349
|
+
let remoteMoves = new Map<string, XYPosition>();
|
|
350
|
+
const settling = new Set<ReturnType<typeof setTimeout>>();
|
|
351
|
+
// Shown gliding, a step a frame: at most one update a frame however many people drag (each presence arriving
|
|
352
|
+
// was an update of its own: ~5 a frame with 7 people dragging), and smooth between their ~30 updates a second.
|
|
353
|
+
// Not what's been dropped since (its drop is in the document, see syncNodes).
|
|
354
|
+
const glide = createSmoother((shown) => {
|
|
355
|
+
const moved = new Map([...shown].filter(([id]) => remoteMoves.has(id)));
|
|
356
|
+
if (moved.size) setNodes((local) => withPositions(local, moved));
|
|
357
|
+
});
|
|
358
|
+
const syncMoves = (_: unknown, origin: unknown) => {
|
|
359
|
+
if (origin === 'local') return;
|
|
360
|
+
const next = new Map<string, XYPosition>();
|
|
361
|
+
awareness.getStates().forEach((state, id) => {
|
|
362
|
+
if (id === doc.clientID || !state.moving) return;
|
|
363
|
+
Object.entries(state.moving as Record<string, XYPosition>).forEach(([nodeId, position]) => next.set(nodeId, position));
|
|
364
|
+
});
|
|
365
|
+
const moved = [...next].some(([id, { x, y }]) => remoteMoves.get(id)?.x !== x || remoteMoves.get(id)?.y !== y);
|
|
366
|
+
const dropped = [...remoteMoves.keys()].filter((id) => !next.has(id));
|
|
367
|
+
remoteMoves = next;
|
|
368
|
+
if (moved || dropped.length) glide.set(next);
|
|
369
|
+
if (dropped.length === 0) return;
|
|
370
|
+
// The drop is saved in the document. If it doesn't arrive, back to where the document has it.
|
|
371
|
+
const settle = setTimeout(() => {
|
|
372
|
+
settling.delete(settle);
|
|
373
|
+
const saved = dropped.flatMap((id) => {
|
|
374
|
+
const node = remoteMoves.has(id) ? undefined : yNodes.get(id);
|
|
375
|
+
return node ? [[id, node.position] as const] : [];
|
|
376
|
+
});
|
|
377
|
+
setNodes((local) => withPositions(local, new Map(saved)));
|
|
378
|
+
}, DROP_SETTLE_MS);
|
|
379
|
+
settling.add(settle);
|
|
380
|
+
};
|
|
381
|
+
awareness.on('change', syncMoves);
|
|
382
|
+
|
|
383
|
+
// What others are writing on notes and text, and how they're scaling and turning text, as they do it (from their
|
|
384
|
+
// presence): shown on just those nodes, and back to what the document has when they're done (their result is
|
|
385
|
+
// written to the document before their presence is cleared, so it doesn't go back in between)
|
|
386
|
+
let remoteEdits = '';
|
|
387
|
+
let remoteEdited = new Set<string>();
|
|
388
|
+
const syncEdits = (_: unknown, origin: unknown) => {
|
|
389
|
+
if (origin === 'local') return;
|
|
390
|
+
const edits = new Map<string, Record<string, unknown>>();
|
|
391
|
+
awareness.getStates().forEach((state, id) => {
|
|
392
|
+
const editing = state.editing as Editing | null | undefined;
|
|
393
|
+
if (id === doc.clientID || !editing?.id) return;
|
|
394
|
+
const { id: nodeId, ...fields } = editing;
|
|
395
|
+
edits.set(nodeId, fields);
|
|
396
|
+
});
|
|
397
|
+
const key = JSON.stringify([...edits]);
|
|
398
|
+
if (key === remoteEdits) return;
|
|
399
|
+
remoteEdits = key;
|
|
400
|
+
const before = remoteEdited;
|
|
401
|
+
remoteEdited = new Set(edits.keys());
|
|
402
|
+
setNodes((local) => {
|
|
403
|
+
let next: Node[] | undefined;
|
|
404
|
+
local.forEach((node, index) => {
|
|
405
|
+
const edit = edits.get(node.id);
|
|
406
|
+
const savedData = before.has(node.id) ? yNodes.get(node.id)?.data : undefined;
|
|
407
|
+
const changes = edit ?? (savedData && Object.fromEntries(EDITED_FIELDS.map((field) => [field, savedData[field]])));
|
|
408
|
+
if (!changes || Object.entries(changes).every(([field, value]) => node.data[field] === value)) return;
|
|
409
|
+
next ??= [...local];
|
|
410
|
+
next[index] = { ...node, data: { ...node.data, ...changes } };
|
|
411
|
+
});
|
|
412
|
+
return next ?? local;
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
awareness.on('change', syncEdits);
|
|
416
|
+
|
|
417
|
+
// Only what changed is replaced, so React Flow re-renders just those nodes
|
|
418
|
+
const syncNodes = (event: Y.YMapEvent<Node>) => {
|
|
419
|
+
if (event.transaction.origin === LOCAL_ORIGIN) return;
|
|
420
|
+
// A node someone was dragging changed in the document: they dropped it there
|
|
421
|
+
event.keysChanged.forEach((id) => remoteMoves.delete(id));
|
|
422
|
+
const containerResized = [...event.changes.keys].some(([id, change]) => {
|
|
423
|
+
const next = yNodes.get(id);
|
|
424
|
+
const previous = change.oldValue as Node | undefined;
|
|
425
|
+
return (
|
|
426
|
+
change.action === 'update' &&
|
|
427
|
+
isGroupType(next?.type) &&
|
|
428
|
+
(next?.width !== previous?.width || next?.height !== previous?.height)
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
if (containerResized) onContainerResizeRef.current?.();
|
|
432
|
+
setNodes((local) =>
|
|
433
|
+
keepDragged(
|
|
434
|
+
patchShared(local, event.keysChanged, (id) => yNodes.get(id), LOCAL_NODE_KEYS),
|
|
435
|
+
local
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
const syncEdges = (event: Y.YMapEvent<Edge>) => {
|
|
440
|
+
if (event.transaction.origin === LOCAL_ORIGIN) return;
|
|
441
|
+
setEdges((local) => patchShared(local, event.keysChanged, (id) => yEdges.get(id), LOCAL_EDGE_KEYS));
|
|
442
|
+
};
|
|
443
|
+
yNodes.observe(syncNodes);
|
|
444
|
+
yEdges.observe(syncEdges);
|
|
445
|
+
|
|
446
|
+
return () => {
|
|
447
|
+
clearTimeout(fallback);
|
|
448
|
+
settling.forEach(clearTimeout);
|
|
449
|
+
window.removeEventListener('online', reconnect);
|
|
450
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
451
|
+
awareness.off('change', syncMoves);
|
|
452
|
+
awareness.off('change', syncEdits);
|
|
453
|
+
glide.stop();
|
|
454
|
+
sender.destroy();
|
|
455
|
+
stopToolSync?.();
|
|
456
|
+
yNodes.unobserve(syncNodes);
|
|
457
|
+
yLevelLayouts.unobserve(syncLevelLayouts);
|
|
458
|
+
yEdges.unobserve(syncEdges);
|
|
459
|
+
yMeta.unobserve(syncMeta);
|
|
460
|
+
yHotspots.unobserve(syncHotspots);
|
|
461
|
+
doc.off('afterTransaction', reopenOnEdit);
|
|
462
|
+
doc.off('afterTransaction', syncStructure);
|
|
463
|
+
presenceStore.destroy();
|
|
464
|
+
undoManager.destroy();
|
|
465
|
+
provider.destroy();
|
|
466
|
+
doc.destroy();
|
|
467
|
+
sharedRef.current = null;
|
|
468
|
+
setDoc(null);
|
|
469
|
+
setPresence(null);
|
|
470
|
+
};
|
|
471
|
+
}, [canvasId, socketUrl, syncViaTools]);
|
|
472
|
+
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
sharedRef.current?.awareness.setLocalStateField('user', user.current);
|
|
475
|
+
}, [name, color, picture]);
|
|
476
|
+
|
|
477
|
+
// Tell others what we have selected so they can see it
|
|
478
|
+
const selectionKey = useMemo(
|
|
479
|
+
() =>
|
|
480
|
+
nodes
|
|
481
|
+
.filter((node) => node.selected)
|
|
482
|
+
.map((node) => node.id)
|
|
483
|
+
.join(','),
|
|
484
|
+
[nodes]
|
|
485
|
+
);
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
sharedRef.current?.sender.send({ selection: selectionKey ? selectionKey.split(',') : [] });
|
|
488
|
+
}, [selectionKey]);
|
|
489
|
+
|
|
490
|
+
// The sizes nodes are rendered at here, for fitting containers to what's in them
|
|
491
|
+
const latestNodes = useRef(nodes);
|
|
492
|
+
latestNodes.current = nodes;
|
|
493
|
+
const renderedSizes = (): RenderedSizes =>
|
|
494
|
+
new Map(
|
|
495
|
+
latestNodes.current.flatMap((node) =>
|
|
496
|
+
node.measured?.width && node.measured.height
|
|
497
|
+
? [[node.id, { width: node.measured.width, height: node.measured.height }] as const]
|
|
498
|
+
: []
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const withDoc = <T>(fn: (doc: Y.Doc) => T, origin: unknown = null) => {
|
|
503
|
+
const doc = sharedRef.current?.doc;
|
|
504
|
+
return doc ? doc.transact(() => fn(doc), origin) : undefined;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Where the nodes being dragged are, until they're dropped, and the containers around them as they've grown
|
|
508
|
+
const dragged = useRef(new Map<string, XYPosition>());
|
|
509
|
+
const dragPreview = useRef(new Map<string, NodePreview>());
|
|
510
|
+
/** Saves where dragged nodes were dropped (in the transaction it's called in), and stops showing them as dragged */
|
|
511
|
+
const saveDrop = (doc: Y.Doc) => {
|
|
512
|
+
dragged.current.forEach((position, id) => moveNode(doc, id, position));
|
|
513
|
+
// Containers as they grew around what was dragged, and what else is in them (moved the other way if they grew up
|
|
514
|
+
// or left). Dragged nodes are where React Flow dropped them, already in their containers' grown coordinates.
|
|
515
|
+
// Only what actually moved or grew is written (the preview has everything in those containers)
|
|
516
|
+
dragPreview.current.forEach(({ position, width, height }, id) =>
|
|
517
|
+
placeNode(doc, id, position, width !== undefined && height !== undefined ? { width, height } : undefined)
|
|
518
|
+
);
|
|
519
|
+
dragPreview.current = new Map();
|
|
520
|
+
dragged.current.clear();
|
|
521
|
+
const shared = sharedRef.current;
|
|
522
|
+
shared?.sender.send({ moving: null });
|
|
523
|
+
// After the drop is in the document (it's sent as the transaction ends)
|
|
524
|
+
queueMicrotask(() => shared?.sender.flush());
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
|
528
|
+
setNodes((local) => applyNodeChanges(changes, local));
|
|
529
|
+
let dropped = false;
|
|
530
|
+
const resized: { id: string; dimensions: { width: number; height: number }; attributes: true | 'width' | 'height' }[] = [];
|
|
531
|
+
const resizeEnded: string[] = [];
|
|
532
|
+
for (const change of changes) {
|
|
533
|
+
if (change.type === 'position' && change.position) {
|
|
534
|
+
dragged.current.set(change.id, change.position);
|
|
535
|
+
if (!change.dragging) dropped = true;
|
|
536
|
+
}
|
|
537
|
+
// Containers resized with their handles
|
|
538
|
+
if (change.type === 'dimensions' && change.setAttributes && change.dimensions)
|
|
539
|
+
resized.push({ id: change.id, dimensions: change.dimensions, attributes: change.setAttributes });
|
|
540
|
+
if (change.type === 'dimensions' && change.resizing === false) resizeEnded.push(change.id);
|
|
541
|
+
}
|
|
542
|
+
// While dragging, nodes move here every frame and go to everyone with our presence; where they're dropped is saved
|
|
543
|
+
if (dropped) {
|
|
544
|
+
// Each drag is its own step to undo
|
|
545
|
+
sharedRef.current?.undoManager.stopCapturing();
|
|
546
|
+
withDoc(saveDrop, LOCAL_ORIGIN);
|
|
547
|
+
} else if (dragged.current.size) sharedRef.current?.sender.send({ moving: Object.fromEntries(dragged.current) });
|
|
548
|
+
if (resized.length)
|
|
549
|
+
withDoc(
|
|
550
|
+
(doc) => resized.forEach(({ id, dimensions, attributes }) => resizeNode(doc, id, dimensions, attributes)),
|
|
551
|
+
LOCAL_ORIGIN
|
|
552
|
+
);
|
|
553
|
+
// Resized inside a container (e.g. a system in a domain): the containers around it grow to fit it. Not as
|
|
554
|
+
// LOCAL_ORIGIN, so they grow here too.
|
|
555
|
+
if (resizeEnded.length) withDoc((doc) => resizeEnded.forEach((id) => fitContainersAround(doc, id, renderedSizes())));
|
|
556
|
+
// Removing a node also removes what's in it, its edges, and moves its comments: those come back through the observers
|
|
557
|
+
const removed = changes.filter((change) => change.type === 'remove').map((change) => change.id);
|
|
558
|
+
if (removed.length) withDoc((doc) => deleteNodesInDoc(doc, removed));
|
|
559
|
+
}, []);
|
|
560
|
+
|
|
561
|
+
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
|
|
562
|
+
setEdges((local) => applyEdgeChanges(changes, local));
|
|
563
|
+
const removed = changes.filter((change) => change.type === 'remove').map((change) => change.id);
|
|
564
|
+
if (removed.length) withDoc((doc) => deleteEdges(doc, removed));
|
|
565
|
+
}, []);
|
|
566
|
+
|
|
567
|
+
const onConnect = useCallback((connection: Connection) => {
|
|
568
|
+
withDoc((doc) => connectNodes(doc, connection));
|
|
569
|
+
}, []);
|
|
570
|
+
|
|
571
|
+
/** A connection's end dragged to another node */
|
|
572
|
+
const onReconnect = useCallback((edge: Edge, connection: Connection) => {
|
|
573
|
+
withDoc((doc) => reconnectEdge(doc, edge.id, connection));
|
|
574
|
+
}, []);
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Containers growing while something's dragged in them, and what's in them, shown here only until it's dropped
|
|
578
|
+
* (see previewContainerGrowth). The drop saves them as shown, in the same change.
|
|
579
|
+
*/
|
|
580
|
+
const showDragPreview = useCallback((previews: Map<string, NodePreview>) => {
|
|
581
|
+
dragPreview.current = previews;
|
|
582
|
+
setNodes((local) => {
|
|
583
|
+
let next: Node[] | undefined;
|
|
584
|
+
local.forEach((node, index) => {
|
|
585
|
+
const preview = previews.get(node.id);
|
|
586
|
+
if (
|
|
587
|
+
!preview ||
|
|
588
|
+
(node.position.x === preview.position.x &&
|
|
589
|
+
node.position.y === preview.position.y &&
|
|
590
|
+
(preview.width === undefined || (node.width === preview.width && node.height === preview.height)))
|
|
591
|
+
)
|
|
592
|
+
return;
|
|
593
|
+
next ??= [...local];
|
|
594
|
+
next[index] = { ...node, ...preview };
|
|
595
|
+
});
|
|
596
|
+
return next ?? local;
|
|
597
|
+
});
|
|
598
|
+
}, []);
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 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).
|
|
603
|
+
*/
|
|
604
|
+
const insertNode = useCallback(
|
|
605
|
+
(
|
|
606
|
+
type: string,
|
|
607
|
+
data: Record<string, unknown>,
|
|
608
|
+
center: XYPosition,
|
|
609
|
+
connect?: (id: string, existing: Node[]) => Edge[],
|
|
610
|
+
inside?: string
|
|
611
|
+
) => {
|
|
612
|
+
const built = buildNode(type, data, center);
|
|
613
|
+
withDoc((doc) => {
|
|
614
|
+
const existing = Array.from(getCanvasMaps(doc).nodes.values());
|
|
615
|
+
// In the container asked for, else the one it's dropped on
|
|
616
|
+
const group = canGoInContainer(type) ? containerFor(center, existing, inside) : undefined;
|
|
617
|
+
const node = group
|
|
618
|
+
? { ...built, parentId: group.id, position: toRelativePosition(built.position, group.id, existing) }
|
|
619
|
+
: built;
|
|
620
|
+
addNodes(doc, [node], connect?.(node.id, existing) ?? []);
|
|
621
|
+
if (group) fitGroupToChildren(doc, group.id, renderedSizes());
|
|
622
|
+
});
|
|
623
|
+
return built.id;
|
|
624
|
+
},
|
|
625
|
+
[]
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
/** 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
|
+
const addNode = useCallback(
|
|
630
|
+
(component: string, center: XYPosition, inside?: string) => {
|
|
631
|
+
const definition = getNodeDefinition(component);
|
|
632
|
+
return definition ? insertNode(definition.type, definition.createData(), center, undefined, inside) : undefined;
|
|
633
|
+
},
|
|
634
|
+
[insertNode]
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const updateNodeData = useCallback(
|
|
638
|
+
(id: string, updater: (data: Record<string, unknown>) => Record<string, unknown>) =>
|
|
639
|
+
withDoc((doc) => updateNodeDataInDoc(doc, id, updater)),
|
|
640
|
+
[]
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const deleteNodes = useCallback((ids: string[]) => withDoc((doc) => deleteNodesInDoc(doc, ids)), []);
|
|
644
|
+
|
|
645
|
+
/** Moves nodes (and resizes containers) from a layout in one change, so it undoes in one go */
|
|
646
|
+
const applyPositions = useCallback((layout: LayoutResult) => withDoc((doc) => applyLayout(doc, layout)), []);
|
|
647
|
+
/** Puts things where they go on L1 or L2: moved there by someone (undone like any edit), or placed by the level */
|
|
648
|
+
const setLevelLayout = useCallback(
|
|
649
|
+
(level: 1 | 2, places: ReadonlyMap<string, LevelPlace>, by: 'person' | 'level' = 'person') =>
|
|
650
|
+
withDoc((doc) => setLevelPlaces(doc, level, places), by === 'level' ? LEVEL_PLACEMENT_ORIGIN : null),
|
|
651
|
+
[]
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
/** Adds ready-made nodes and edges (e.g. a container with what's in it) in one change */
|
|
655
|
+
const insertNodes = useCallback(
|
|
656
|
+
(nodes: Node[], edges: Edge[] = []) =>
|
|
657
|
+
withDoc((doc) => {
|
|
658
|
+
addNodes(doc, nodes, edges);
|
|
659
|
+
// Containers already on the canvas that got something new inside grow to fit it
|
|
660
|
+
const added = new Set(nodes.map((node) => node.id));
|
|
661
|
+
new Set(nodes.flatMap((node) => (node.parentId && !added.has(node.parentId) ? [node.parentId] : []))).forEach((groupId) =>
|
|
662
|
+
fitGroupToChildren(doc, groupId, renderedSizes())
|
|
663
|
+
);
|
|
664
|
+
}),
|
|
665
|
+
[]
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
/** Puts nodes in containers (or takes them out) together, e.g. a selection dropped in one, as one change */
|
|
669
|
+
const setParents = useCallback(
|
|
670
|
+
(moves: { id: string; parentId: string | undefined; at?: XYPosition }[]) =>
|
|
671
|
+
withDoc((doc) => {
|
|
672
|
+
if (dragged.current.size) saveDrop(doc);
|
|
673
|
+
return setNodeParents(doc, moves, renderedSizes());
|
|
674
|
+
}),
|
|
675
|
+
[]
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
/** Changes the canvas's status for everyone (recorded with who did it, and why if given) */
|
|
679
|
+
const changeStatus = useCallback(
|
|
680
|
+
(status: CanvasStatus, note?: string) => withDoc((doc) => setCanvasStatus(doc, status, user.current, note), STATUS_ORIGIN),
|
|
681
|
+
[]
|
|
682
|
+
);
|
|
683
|
+
const rename = useCallback((next: string) => withDoc((doc) => setMeta(doc, { title: next || undefined })), []);
|
|
684
|
+
|
|
685
|
+
const undo = useCallback(() => sharedRef.current?.undoManager.undo(), []);
|
|
686
|
+
const redo = useCallback(() => sharedRef.current?.undoManager.redo(), []);
|
|
687
|
+
|
|
688
|
+
/** Our pointer on the canvas, for everyone (sent about 30 times a second, so call it on every move) */
|
|
689
|
+
const setPointer = useCallback((pointer: XYPosition | null) => {
|
|
690
|
+
sharedRef.current?.sender.send({ pointer });
|
|
691
|
+
}, []);
|
|
692
|
+
|
|
693
|
+
const setViewport = useCallback((viewport: { x: number; y: number; zoom: number }) => {
|
|
694
|
+
sharedRef.current?.sender.send({ viewport });
|
|
695
|
+
}, []);
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The note or text being changed here, as it changes (what's written, a text's size and angle), for the others to
|
|
699
|
+
* see as it happens; `null` when done (after the result is saved), sent straight away
|
|
700
|
+
*/
|
|
701
|
+
const sendEditing = useCallback((editing: Editing | null) => {
|
|
702
|
+
const sender = sharedRef.current?.sender;
|
|
703
|
+
if (!sender) return;
|
|
704
|
+
sender.send({ editing: editing && { ...editing, ...(editing.text !== undefined && { text: editing.text.slice(0, 2000) }) } });
|
|
705
|
+
if (!editing) sender.flush();
|
|
706
|
+
}, []);
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* What a diagram file opened into this (new) canvas starts with: its nodes and connections, comments, hotspots and
|
|
710
|
+
* where things are on L1 and L2, in one change that isn't undone
|
|
711
|
+
*/
|
|
712
|
+
const addContent = useCallback(
|
|
713
|
+
(content: ModelCanvasContent) =>
|
|
714
|
+
withDoc((doc) => {
|
|
715
|
+
addNodes(doc, content.nodes, content.edges);
|
|
716
|
+
if (content.threads?.length) addThreads(doc, content.threads);
|
|
717
|
+
if (content.hotspots?.length) addHotspots(doc, content.hotspots);
|
|
718
|
+
setLevelPlaces(doc, 1, content.levelLayouts[1]);
|
|
719
|
+
setLevelPlaces(doc, 2, content.levelLayouts[2]);
|
|
720
|
+
}, OPEN_ORIGIN),
|
|
721
|
+
[]
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// ---- Hotspots and the lock ----
|
|
725
|
+
|
|
726
|
+
const createHotspot = useCallback(
|
|
727
|
+
(hotspot: Omit<Hotspot, 'id' | 'createdAt'>) => withDoc((doc) => createHotspotInDoc(doc, hotspot)),
|
|
728
|
+
[]
|
|
729
|
+
);
|
|
730
|
+
const updateHotspot = useCallback(
|
|
731
|
+
(id: string, fields: Parameters<typeof updateHotspotInDoc>[2]) => withDoc((doc) => updateHotspotInDoc(doc, id, fields)),
|
|
732
|
+
[]
|
|
733
|
+
);
|
|
734
|
+
const deleteHotspot = useCallback((id: string) => withDoc((doc) => deleteHotspotInDoc(doc, id)), []);
|
|
735
|
+
/** Locks the canvas for everyone (by a name), or unlocks it (null): looked at, not changed */
|
|
736
|
+
const setLocked = useCallback((by: string | null) => withDoc((doc) => setLock(doc, by), STATUS_ORIGIN), []);
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
nodes,
|
|
740
|
+
edges,
|
|
741
|
+
status,
|
|
742
|
+
presence,
|
|
743
|
+
clientId,
|
|
744
|
+
doc,
|
|
745
|
+
onNodesChange,
|
|
746
|
+
onEdgesChange,
|
|
747
|
+
onConnect,
|
|
748
|
+
onReconnect,
|
|
749
|
+
showDragPreview,
|
|
750
|
+
addNode,
|
|
751
|
+
insertNode,
|
|
752
|
+
updateNodeData,
|
|
753
|
+
deleteNodes,
|
|
754
|
+
applyPositions,
|
|
755
|
+
levelLayouts,
|
|
756
|
+
setLevelLayout,
|
|
757
|
+
insertNodes,
|
|
758
|
+
setParents,
|
|
759
|
+
title,
|
|
760
|
+
diagramLevels,
|
|
761
|
+
structureKey,
|
|
762
|
+
rename,
|
|
763
|
+
canvasStatus,
|
|
764
|
+
changeStatus,
|
|
765
|
+
transport,
|
|
766
|
+
deleted,
|
|
767
|
+
hotspots,
|
|
768
|
+
createHotspot,
|
|
769
|
+
updateHotspot,
|
|
770
|
+
deleteHotspot,
|
|
771
|
+
lockedBy,
|
|
772
|
+
setLocked,
|
|
773
|
+
sendEditing,
|
|
774
|
+
synced,
|
|
775
|
+
addContent,
|
|
776
|
+
undo,
|
|
777
|
+
redo,
|
|
778
|
+
...history,
|
|
779
|
+
setPointer,
|
|
780
|
+
setViewport,
|
|
781
|
+
};
|
|
782
|
+
}
|