@eventcatalog/core 3.10.2 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-K7EPHS7S.js → chunk-G7GG3HEB.js} +1 -1
- package/dist/{chunk-DGE5ITBR.js → chunk-K44BXVHU.js} +1 -1
- package/dist/{chunk-IJKRHWWO.js → chunk-LXOS3MXQ.js} +1 -1
- package/dist/{chunk-QWSUFHCT.js → chunk-VUBZ6A7B.js} +1 -1
- package/dist/{chunk-AL67CV2N.js → chunk-WVKLG26T.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -1
- package/dist/eventcatalog.js +6 -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/integrations/eventcatalog-features.ts +13 -0
- package/eventcatalog/src/components/MDX/Design/Design.astro +10 -2
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +10 -2
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +10 -2
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +13 -0
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +294 -0
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +92 -0
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +26 -0
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +163 -0
- package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +99 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -2
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +166 -43
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +4 -1
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +4 -1
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +4 -1
- package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +91 -2
- package/eventcatalog/src/enterprise/visualizer-layout/reset.ts +45 -0
- package/eventcatalog/src/enterprise/visualizer-layout/save.ts +57 -0
- package/eventcatalog/src/layouts/Footer.astro +4 -1
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +3 -1
- package/eventcatalog/src/utils/feature.ts +2 -0
- package/eventcatalog/src/utils/node-graphs/layout-persistence.ts +81 -0
- package/eventcatalog/tailwind.config.mjs +10 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-VUBZ6A7B.js";
|
|
4
|
+
import "../chunk-K44BXVHU.js";
|
|
5
5
|
import "../chunk-4UVFXLPI.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-G7GG3HEB.js";
|
|
7
7
|
import "../chunk-UPONRQSN.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -109,7 +109,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
109
109
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
110
110
|
|
|
111
111
|
// package.json
|
|
112
|
-
var version = "3.
|
|
112
|
+
var version = "3.12.0";
|
|
113
113
|
|
|
114
114
|
// src/constants.ts
|
|
115
115
|
var VERSION = version;
|
|
@@ -812,6 +812,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
812
812
|
ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
|
|
813
813
|
EVENTCATALOG_STARTER: isEventCatalogStarter,
|
|
814
814
|
EVENTCATALOG_SCALE: isEventCatalogScale,
|
|
815
|
+
EVENTCATALOG_DEV_MODE: "true",
|
|
815
816
|
NODE_NO_WARNINGS: "1"
|
|
816
817
|
}
|
|
817
818
|
}
|
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-PLNJC7NZ.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-VUBZ6A7B.js";
|
|
10
|
+
import "./chunk-K44BXVHU.js";
|
|
11
11
|
import "./chunk-4UVFXLPI.js";
|
|
12
12
|
import {
|
|
13
13
|
runMigrations
|
|
@@ -22,13 +22,13 @@ import {
|
|
|
22
22
|
} from "./chunk-5VBIXL6C.js";
|
|
23
23
|
import {
|
|
24
24
|
generate
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-LXOS3MXQ.js";
|
|
26
26
|
import {
|
|
27
27
|
logger
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-WVKLG26T.js";
|
|
29
29
|
import {
|
|
30
30
|
VERSION
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-G7GG3HEB.js";
|
|
32
32
|
import "./chunk-UPONRQSN.js";
|
|
33
33
|
|
|
34
34
|
// src/eventcatalog.ts
|
|
@@ -165,6 +165,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
165
165
|
ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
|
|
166
166
|
EVENTCATALOG_STARTER: isEventCatalogStarter,
|
|
167
167
|
EVENTCATALOG_SCALE: isEventCatalogScale,
|
|
168
|
+
EVENTCATALOG_DEV_MODE: "true",
|
|
168
169
|
NODE_NO_WARNINGS: "1"
|
|
169
170
|
}
|
|
170
171
|
}
|
package/dist/generate.cjs
CHANGED
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-LXOS3MXQ.js";
|
|
4
|
+
import "./chunk-WVKLG26T.js";
|
|
5
|
+
import "./chunk-G7GG3HEB.js";
|
|
6
6
|
import "./chunk-UPONRQSN.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isEventCatalogScaleEnabled,
|
|
7
7
|
isEventCatalogStarterEnabled,
|
|
8
8
|
isEventCatalogMCPEnabled,
|
|
9
|
+
isDevMode,
|
|
9
10
|
} from '../src/utils/feature';
|
|
10
11
|
|
|
11
12
|
const catalogDirectory = process.env.CATALOG_DIR || process.cwd();
|
|
@@ -72,6 +73,18 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
72
73
|
entrypoint: path.join(catalogDirectory, 'src/enterprise/plans/index.astro'),
|
|
73
74
|
});
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
// Dev-only routes for visualizer layout persistence
|
|
78
|
+
if (isDevMode()) {
|
|
79
|
+
params.injectRoute({
|
|
80
|
+
pattern: '/api/dev/visualizer-layout/save',
|
|
81
|
+
entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/save.ts'),
|
|
82
|
+
});
|
|
83
|
+
params.injectRoute({
|
|
84
|
+
pattern: '/api/dev/visualizer-layout/reset',
|
|
85
|
+
entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/reset.ts'),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
75
88
|
},
|
|
76
89
|
},
|
|
77
90
|
};
|
|
@@ -5,7 +5,8 @@ import { getAbsoluteFilePathForAstroFile } from '@utils/files';
|
|
|
5
5
|
import Admonition from '@components/MDX/Admonition';
|
|
6
6
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
7
7
|
|
|
8
|
-
import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
|
|
8
|
+
import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
9
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
9
10
|
|
|
10
11
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
11
12
|
|
|
@@ -21,6 +22,11 @@ try {
|
|
|
21
22
|
} catch (error) {
|
|
22
23
|
console.error(`Error reading design file: ${error}`);
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
// Load and apply saved layout if it exists
|
|
27
|
+
const resourceKey = design ? buildResourceKey('designs', design.id || id) : '';
|
|
28
|
+
const savedLayout = resourceKey ? await loadSavedLayout(resourceKey) : null;
|
|
29
|
+
const nodesWithLayout = design ? applyLayoutToNodes(design.nodes || [], savedLayout) : ([] as any[]);
|
|
24
30
|
---
|
|
25
31
|
|
|
26
32
|
{
|
|
@@ -51,7 +57,7 @@ try {
|
|
|
51
57
|
<NodeGraph
|
|
52
58
|
id={id}
|
|
53
59
|
title={title ?? design.name}
|
|
54
|
-
nodes={
|
|
60
|
+
nodes={nodesWithLayout}
|
|
55
61
|
edges={design.edges || []}
|
|
56
62
|
hrefLabel={isVisualiserEnabled() ? 'View in visualizer' : undefined}
|
|
57
63
|
href={isVisualiserEnabled() ? `/visualiser/designs/${design.id}` : undefined}
|
|
@@ -60,6 +66,8 @@ try {
|
|
|
60
66
|
client:only="react"
|
|
61
67
|
showSearch={search}
|
|
62
68
|
isChatEnabled={isChatEnabled}
|
|
69
|
+
isDevMode={isDevMode()}
|
|
70
|
+
resourceKey={resourceKey}
|
|
63
71
|
/>
|
|
64
72
|
</div>
|
|
65
73
|
</div>
|
|
@@ -5,7 +5,8 @@ import Admonition from '@components/MDX/Admonition';
|
|
|
5
5
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
6
6
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
7
|
import { getServices } from '@utils/collections/services';
|
|
8
|
-
import { isEventCatalogChatEnabled } from '@utils/feature';
|
|
8
|
+
import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
9
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
9
10
|
|
|
10
11
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
11
12
|
|
|
@@ -38,6 +39,11 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
38
39
|
...(entities ? { entities } : {}), // Pass entities if provided
|
|
39
40
|
type: collection,
|
|
40
41
|
});
|
|
42
|
+
|
|
43
|
+
// Load and apply saved layout if it exists
|
|
44
|
+
const resourceKey = buildResourceKey(collection, resourceId, resource?.data?.version);
|
|
45
|
+
const savedLayout = await loadSavedLayout(resourceKey);
|
|
46
|
+
const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
|
|
41
47
|
---
|
|
42
48
|
|
|
43
49
|
{
|
|
@@ -66,7 +72,7 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
66
72
|
<div>
|
|
67
73
|
<NodeGraph
|
|
68
74
|
id={id}
|
|
69
|
-
nodes={
|
|
75
|
+
nodes={nodesWithLayout}
|
|
70
76
|
edges={edges}
|
|
71
77
|
linkTo={'visualiser'}
|
|
72
78
|
mode="simple"
|
|
@@ -75,6 +81,8 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
75
81
|
client:only="react"
|
|
76
82
|
portalId={`${id}-entity-map-portal`}
|
|
77
83
|
isChatEnabled={isChatEnabled}
|
|
84
|
+
isDevMode={isDevMode()}
|
|
85
|
+
resourceKey={resourceKey}
|
|
78
86
|
/>
|
|
79
87
|
</div>
|
|
80
88
|
|
|
@@ -4,7 +4,8 @@ import { getNodesAndEdges } from '@utils/node-graphs/flows-node-graph';
|
|
|
4
4
|
import Admonition from '@components/MDX/Admonition';
|
|
5
5
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
6
6
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
|
-
import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
|
|
7
|
+
import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
8
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
8
9
|
|
|
9
10
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
10
11
|
|
|
@@ -22,6 +23,11 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
22
23
|
version: flow.data.version,
|
|
23
24
|
mode: mode,
|
|
24
25
|
});
|
|
26
|
+
|
|
27
|
+
// Load and apply saved layout if it exists
|
|
28
|
+
const resourceKey = buildResourceKey('flows', id, flow.data.version);
|
|
29
|
+
const savedLayout = await loadSavedLayout(resourceKey);
|
|
30
|
+
const nodesWithLayout = applyLayoutToNodes(nodes, savedLayout);
|
|
25
31
|
---
|
|
26
32
|
|
|
27
33
|
{
|
|
@@ -49,7 +55,7 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
49
55
|
<div>
|
|
50
56
|
<NodeGraph
|
|
51
57
|
id={id}
|
|
52
|
-
nodes={
|
|
58
|
+
nodes={nodesWithLayout}
|
|
53
59
|
edges={edges}
|
|
54
60
|
hrefLabel={'View in visualizer'}
|
|
55
61
|
href={isVisualiserEnabled() ? `/visualiser/flows/${id}/${version}` : undefined}
|
|
@@ -60,6 +66,8 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
60
66
|
showFlowWalkthrough={walkthrough}
|
|
61
67
|
showSearch={search}
|
|
62
68
|
isChatEnabled={isChatEnabled}
|
|
69
|
+
isDevMode={isDevMode()}
|
|
70
|
+
resourceKey={resourceKey}
|
|
63
71
|
/>
|
|
64
72
|
</div>
|
|
65
73
|
|
|
@@ -81,6 +81,19 @@ const AnimatedMessageEdge = ({
|
|
|
81
81
|
{/* <g className={`z-30 ${opacity === 1 ? 'opacity-100' : 'opacity-10'}`}>
|
|
82
82
|
</g> */}
|
|
83
83
|
<g>
|
|
84
|
+
{/* Background rect for label */}
|
|
85
|
+
{label && (
|
|
86
|
+
<rect
|
|
87
|
+
x={labelX - 30}
|
|
88
|
+
y={labelY - lines.length * 6}
|
|
89
|
+
width={60}
|
|
90
|
+
height={lines.length * 14}
|
|
91
|
+
fill="white"
|
|
92
|
+
fillOpacity={0.3}
|
|
93
|
+
rx={4}
|
|
94
|
+
ry={4}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
84
97
|
{/* Text element */}
|
|
85
98
|
<text x={labelX} y={labelY} textAnchor="middle" dominantBaseline="middle" fontSize="10px" pointerEvents="none">
|
|
86
99
|
{lines.map((line, i) => (
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React, { useMemo, useCallback, useEffect, useState, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
Background,
|
|
5
|
+
Controls,
|
|
6
|
+
useReactFlow,
|
|
7
|
+
useNodesState,
|
|
8
|
+
useEdgesState,
|
|
9
|
+
type Node,
|
|
10
|
+
type Edge,
|
|
11
|
+
type NodeTypes,
|
|
12
|
+
type EdgeTypes,
|
|
13
|
+
} from '@xyflow/react';
|
|
14
|
+
import { getConnectedNodes, getNodeDisplayInfo } from './utils';
|
|
15
|
+
import FocusModeNodeActions from './FocusModeNodeActions';
|
|
16
|
+
import FocusModePlaceholder from './FocusModePlaceholder';
|
|
17
|
+
|
|
18
|
+
interface FocusModeContentProps {
|
|
19
|
+
centerNodeId: string;
|
|
20
|
+
nodes: Node[];
|
|
21
|
+
edges: Edge[];
|
|
22
|
+
nodeTypes: NodeTypes;
|
|
23
|
+
edgeTypes: EdgeTypes;
|
|
24
|
+
onSwitchCenter: (newCenterNodeId: string, direction: 'left' | 'right') => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HORIZONTAL_SPACING = 450;
|
|
28
|
+
const VERTICAL_SPACING = 200;
|
|
29
|
+
const SLIDE_DURATION = 300;
|
|
30
|
+
|
|
31
|
+
const FocusModeContent: React.FC<FocusModeContentProps> = ({
|
|
32
|
+
centerNodeId,
|
|
33
|
+
nodes: allNodes,
|
|
34
|
+
edges: allEdges,
|
|
35
|
+
nodeTypes,
|
|
36
|
+
edgeTypes,
|
|
37
|
+
onSwitchCenter,
|
|
38
|
+
}) => {
|
|
39
|
+
const { fitView } = useReactFlow();
|
|
40
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
41
|
+
const [needsFitView, setNeedsFitView] = useState(false);
|
|
42
|
+
const [hoveredEdgeId, setHoveredEdgeId] = useState<string | null>(null);
|
|
43
|
+
const reactFlowInitialized = useRef(false);
|
|
44
|
+
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
45
|
+
|
|
46
|
+
// Cleanup timeout on unmount
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
return () => {
|
|
49
|
+
if (animationTimeoutRef.current) {
|
|
50
|
+
clearTimeout(animationTimeoutRef.current);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// Calculate focused nodes and edges with positions
|
|
56
|
+
const calculateFocusedGraph = useCallback(
|
|
57
|
+
(centerId: string) => {
|
|
58
|
+
const centerNode = allNodes.find((n) => n.id === centerId);
|
|
59
|
+
if (!centerNode) {
|
|
60
|
+
return { nodes: [], edges: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { leftNodes, rightNodes } = getConnectedNodes(centerId, allNodes, allEdges);
|
|
64
|
+
const centerNodeInfo = getNodeDisplayInfo(centerNode);
|
|
65
|
+
const positionedNodes: Node[] = [];
|
|
66
|
+
|
|
67
|
+
// Center node at origin
|
|
68
|
+
positionedNodes.push({
|
|
69
|
+
...centerNode,
|
|
70
|
+
position: { x: 0, y: 0 },
|
|
71
|
+
style: { ...centerNode.style, opacity: 1 },
|
|
72
|
+
data: { ...centerNode.data, isFocusCenter: true },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Left nodes (incoming)
|
|
76
|
+
leftNodes.forEach((node, index) => {
|
|
77
|
+
const yOffset = (index - (leftNodes.length - 1) / 2) * VERTICAL_SPACING;
|
|
78
|
+
positionedNodes.push({
|
|
79
|
+
...node,
|
|
80
|
+
position: { x: -HORIZONTAL_SPACING, y: yOffset },
|
|
81
|
+
style: { ...node.style, opacity: 1 },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Right nodes (outgoing)
|
|
86
|
+
rightNodes.forEach((node, index) => {
|
|
87
|
+
const yOffset = (index - (rightNodes.length - 1) / 2) * VERTICAL_SPACING;
|
|
88
|
+
positionedNodes.push({
|
|
89
|
+
...node,
|
|
90
|
+
position: { x: HORIZONTAL_SPACING, y: yOffset },
|
|
91
|
+
style: { ...node.style, opacity: 1 },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Add placeholder nodes if no connections exist
|
|
96
|
+
if (leftNodes.length === 0) {
|
|
97
|
+
positionedNodes.push({
|
|
98
|
+
id: '__placeholder-left__',
|
|
99
|
+
type: 'placeholder',
|
|
100
|
+
position: { x: -HORIZONTAL_SPACING, y: 0 },
|
|
101
|
+
data: { label: `No inputs found for "${centerNodeInfo.name}" in this diagram`, side: 'left' },
|
|
102
|
+
draggable: false,
|
|
103
|
+
selectable: false,
|
|
104
|
+
} as Node);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (rightNodes.length === 0) {
|
|
108
|
+
positionedNodes.push({
|
|
109
|
+
id: '__placeholder-right__',
|
|
110
|
+
type: 'placeholder',
|
|
111
|
+
position: { x: HORIZONTAL_SPACING, y: 0 },
|
|
112
|
+
data: { label: `No outputs found for "${centerNodeInfo.name}" in this diagram`, side: 'right' },
|
|
113
|
+
draggable: false,
|
|
114
|
+
selectable: false,
|
|
115
|
+
} as Node);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Filter edges - only show edges connected to center node
|
|
119
|
+
const focusedNodeIds = new Set(positionedNodes.map((n) => n.id));
|
|
120
|
+
const filteredEdges = allEdges
|
|
121
|
+
.filter((edge) => {
|
|
122
|
+
// Only include edges where center node is either source or target
|
|
123
|
+
const connectsToCenter = edge.source === centerId || edge.target === centerId;
|
|
124
|
+
// And the other end is in our focused nodes
|
|
125
|
+
const otherEndInFocus = focusedNodeIds.has(edge.source) && focusedNodeIds.has(edge.target);
|
|
126
|
+
return connectsToCenter && otherEndInFocus;
|
|
127
|
+
})
|
|
128
|
+
.map((edge) => ({
|
|
129
|
+
...edge,
|
|
130
|
+
style: { ...edge.style, opacity: 1 },
|
|
131
|
+
labelStyle: { ...edge.labelStyle, opacity: 1 },
|
|
132
|
+
data: { ...edge.data, opacity: 1, animated: false },
|
|
133
|
+
animated: false,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return { nodes: positionedNodes, edges: filteredEdges };
|
|
137
|
+
},
|
|
138
|
+
[allNodes, allEdges]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Initial graph
|
|
142
|
+
const initialGraph = useMemo(() => calculateFocusedGraph(centerNodeId), [centerNodeId, calculateFocusedGraph]);
|
|
143
|
+
|
|
144
|
+
const [displayNodes, setDisplayNodes] = useNodesState(initialGraph.nodes);
|
|
145
|
+
const [displayEdges, setDisplayEdges] = useEdgesState(initialGraph.edges);
|
|
146
|
+
|
|
147
|
+
// Update when centerNodeId changes externally
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const { nodes, edges } = calculateFocusedGraph(centerNodeId);
|
|
150
|
+
setDisplayNodes(nodes);
|
|
151
|
+
setDisplayEdges(edges);
|
|
152
|
+
setNeedsFitView(true);
|
|
153
|
+
}, [centerNodeId, calculateFocusedGraph, setDisplayNodes, setDisplayEdges]);
|
|
154
|
+
|
|
155
|
+
// FitView when needed - triggered after nodes are updated
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (needsFitView && reactFlowInitialized.current) {
|
|
158
|
+
// Wait for nodes to be fully rendered, then fit view with animation
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
fitView({ padding: 0.2, duration: 400 });
|
|
161
|
+
setNeedsFitView(false);
|
|
162
|
+
}, 150);
|
|
163
|
+
return () => clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
}, [needsFitView, displayNodes, fitView]);
|
|
166
|
+
|
|
167
|
+
// Handle ReactFlow initialization
|
|
168
|
+
const handleInit = useCallback(() => {
|
|
169
|
+
reactFlowInitialized.current = true;
|
|
170
|
+
// Wait for nodes to be fully rendered, then fit view with animation
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
fitView({ padding: 0.2, duration: 400 });
|
|
173
|
+
}, 150);
|
|
174
|
+
}, [fitView]);
|
|
175
|
+
|
|
176
|
+
// Handle switching to a new center node with animation
|
|
177
|
+
const handleSwitchNode = useCallback(
|
|
178
|
+
(nodeId: string, direction: 'left' | 'right') => {
|
|
179
|
+
if (nodeId === centerNodeId || isAnimating) return;
|
|
180
|
+
|
|
181
|
+
setIsAnimating(true);
|
|
182
|
+
|
|
183
|
+
// Animate: clicked node slides to center, current center hides
|
|
184
|
+
setDisplayNodes((currentNodes) =>
|
|
185
|
+
currentNodes.map((node) => {
|
|
186
|
+
if (node.id === nodeId) {
|
|
187
|
+
// Clicked node slides to center
|
|
188
|
+
return {
|
|
189
|
+
...node,
|
|
190
|
+
position: { x: 0, y: 0 },
|
|
191
|
+
style: { ...node.style, transition: `all ${SLIDE_DURATION}ms ease-out` },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (node.id === centerNodeId) {
|
|
195
|
+
// Current center node hides
|
|
196
|
+
return {
|
|
197
|
+
...node,
|
|
198
|
+
style: { ...node.style, opacity: 0, transition: `opacity ${SLIDE_DURATION}ms ease-out` },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return node;
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// After slide completes, switch to new center
|
|
206
|
+
animationTimeoutRef.current = setTimeout(() => {
|
|
207
|
+
onSwitchCenter(nodeId, direction);
|
|
208
|
+
setIsAnimating(false);
|
|
209
|
+
}, SLIDE_DURATION);
|
|
210
|
+
},
|
|
211
|
+
[centerNodeId, isAnimating, setDisplayNodes, onSwitchCenter]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Handle node click with animation
|
|
215
|
+
const handleNodeClick = useCallback(
|
|
216
|
+
(_: React.MouseEvent, clickedNode: Node) => {
|
|
217
|
+
if (clickedNode.id === centerNodeId || isAnimating) return;
|
|
218
|
+
const direction = (clickedNode.position?.x ?? 0) < 0 ? 'left' : 'right';
|
|
219
|
+
handleSwitchNode(clickedNode.id, direction);
|
|
220
|
+
},
|
|
221
|
+
[centerNodeId, isAnimating, handleSwitchNode]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Handle edge hover for animation
|
|
225
|
+
const handleEdgeMouseEnter = useCallback((_: React.MouseEvent, edge: Edge) => {
|
|
226
|
+
setHoveredEdgeId(edge.id);
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
const handleEdgeMouseLeave = useCallback(() => {
|
|
230
|
+
setHoveredEdgeId(null);
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
// Apply hover animation to edges - use ReactFlow's built-in animated property
|
|
234
|
+
const edgesWithHover = useMemo(() => {
|
|
235
|
+
return displayEdges.map((edge) => {
|
|
236
|
+
if (edge.id === hoveredEdgeId) {
|
|
237
|
+
return {
|
|
238
|
+
...edge,
|
|
239
|
+
animated: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return edge;
|
|
243
|
+
});
|
|
244
|
+
}, [displayEdges, hoveredEdgeId]);
|
|
245
|
+
|
|
246
|
+
// Merge nodeTypes with placeholder
|
|
247
|
+
const mergedNodeTypes = useMemo(
|
|
248
|
+
() => ({
|
|
249
|
+
...nodeTypes,
|
|
250
|
+
placeholder: FocusModePlaceholder,
|
|
251
|
+
}),
|
|
252
|
+
[nodeTypes]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (displayNodes.length === 0) {
|
|
256
|
+
return <div className="flex items-center justify-center h-full text-[rgb(var(--ec-page-text-muted))]">Node not found</div>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="h-full w-full focus-mode-container">
|
|
261
|
+
<ReactFlow
|
|
262
|
+
nodes={displayNodes}
|
|
263
|
+
edges={edgesWithHover}
|
|
264
|
+
nodeTypes={mergedNodeTypes}
|
|
265
|
+
edgeTypes={edgeTypes}
|
|
266
|
+
onNodeClick={handleNodeClick}
|
|
267
|
+
onEdgeMouseEnter={handleEdgeMouseEnter}
|
|
268
|
+
onEdgeMouseLeave={handleEdgeMouseLeave}
|
|
269
|
+
onInit={handleInit}
|
|
270
|
+
proOptions={{ hideAttribution: true }}
|
|
271
|
+
nodesDraggable={true}
|
|
272
|
+
nodesConnectable={false}
|
|
273
|
+
elementsSelectable={true}
|
|
274
|
+
panOnDrag={true}
|
|
275
|
+
zoomOnScroll={true}
|
|
276
|
+
minZoom={0.3}
|
|
277
|
+
maxZoom={2}
|
|
278
|
+
>
|
|
279
|
+
<Background color="rgb(var(--ec-page-border))" gap={20} />
|
|
280
|
+
<Controls showInteractive={false} />
|
|
281
|
+
{displayNodes.map((node, index) => (
|
|
282
|
+
<FocusModeNodeActions
|
|
283
|
+
key={`actions-${node.id}-${index}`}
|
|
284
|
+
node={node}
|
|
285
|
+
isCenter={node.id === centerNodeId}
|
|
286
|
+
onSwitch={handleSwitchNode}
|
|
287
|
+
/>
|
|
288
|
+
))}
|
|
289
|
+
</ReactFlow>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export default FocusModeContent;
|