diff --git a/packages/graph-editor/src/components/panels/index.tsx b/packages/graph-editor/src/components/panels/index.tsx
index af9aad22d..3ac76bf0a 100644
--- a/packages/graph-editor/src/components/panels/index.tsx
+++ b/packages/graph-editor/src/components/panels/index.tsx
@@ -9,3 +9,4 @@ export * from './nodeSettings/index.js';
export * from './output/index.js';
export * from './play/index.js';
export * from './settings/index.js';
+export * from './navigation/index.js';
diff --git a/packages/graph-editor/src/components/panels/navigation/index.module.css b/packages/graph-editor/src/components/panels/navigation/index.module.css
new file mode 100644
index 000000000..b24e91c10
--- /dev/null
+++ b/packages/graph-editor/src/components/panels/navigation/index.module.css
@@ -0,0 +1,38 @@
+.scrollWrapper {
+ height: 100%;
+ flex: 1;
+ padding: var(--component-spacing-md);
+ overflow: auto;
+}
+
+.componentSpacingWrapper {
+ padding: var(--component-spacing-md);
+}
+
+.listWrapper {
+ display: flex;
+ flex-direction: column;
+ font-size: var(--font-body-small-default);
+ gap: var(--component-spacing-xs);
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ user-select: none;
+}
+
+.listItem {
+ cursor: pointer;
+ display: inline-flex;
+ gap: var(--component-spacing-sm);
+ padding-left: 0;
+ padding-left: calc(var(--tree-depth, 0px) * var(--component-spacing-xs, 1px));
+}
+
+.listItemSelected {
+ font-weight: bold;
+}
+
+.listItemCount {
+ color: var(--fg-subtle);
+ font-weight: normal;
+}
diff --git a/packages/graph-editor/src/components/panels/navigation/index.tsx b/packages/graph-editor/src/components/panels/navigation/index.tsx
new file mode 100644
index 000000000..bab3f8e78
--- /dev/null
+++ b/packages/graph-editor/src/components/panels/navigation/index.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+
+import { MAIN_GRAPH_ID } from '@/constants.js';
+import { Stack } from '@tokens-studio/ui';
+import { TreeNode, graphNodesSelector } from '@/redux/selectors/graph.js';
+import { currentPanelIdSelector } from '@/redux/selectors/graph.js';
+import { dockerSelector } from '@/redux/selectors/refs.js';
+import { flow, get, size } from 'lodash-es';
+import { useSelector } from 'react-redux';
+import { useSubgraphExplorerCallback } from '@/hooks/useSubgraphExplorerCallback.js';
+import cx from 'classnames';
+import styles from './index.module.css';
+
+type ListItemProps = {
+ label: string;
+ count?: number;
+ isSelected?: boolean;
+ onClick?: () => void;
+ depth?: number;
+};
+
+const ListItem = function ({
+ label,
+ count,
+ isSelected,
+ onClick,
+ depth = 0,
+}: ListItemProps) {
+ const style = {
+ '--tree-depth': depth,
+ } as React.CSSProperties;
+
+ return (
+
+ {label}
+ {count && ({count})}
+
+ );
+};
+
+const SubgraphNodeItem = function ({ node, isSelected, depth }) {
+ const nodeType = node.factory.title || node.nodeType();
+ const onNodeClick = useSubgraphExplorerCallback(node);
+ const childNodesCount = flow(
+ (x) => get(x, ['_innerGraph', 'nodes'], []),
+ size,
+ )(node);
+
+ return (
+ 0 && childNodesCount}
+ />
+ );
+};
+
+const RootGraphNodeItem = function () {
+ const dockerRef = useSelector(dockerSelector);
+ const activeGraphId = useSelector(currentPanelIdSelector);
+
+ return (
+ dockerRef.current.updateTab(MAIN_GRAPH_ID, null, true)}
+ />
+ );
+};
+
+export const NavigationPanel = () => {
+ const nodes = useSelector(graphNodesSelector);
+ const activeGraphId = useSelector(currentPanelIdSelector);
+
+ return (
+
+
+
+
+ {Object.values(nodes || {}).map(({ node, depth }: TreeNode) => {
+ const innerGraph = node['_innerGraph'];
+ if (!innerGraph) return null;
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx b/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx
index 4a51af251..47b3d275c 100644
--- a/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx
+++ b/packages/graph-editor/src/components/toolbar/dropdowns/layout.tsx
@@ -77,6 +77,9 @@ export const LayoutDropdown = () => {
onClick('dropPanel')}>
Nodes
+ onClick('navigationPanel')}>
+ Navigator
+
diff --git a/packages/graph-editor/src/components/toolbar/layoutButtons.tsx b/packages/graph-editor/src/components/toolbar/layoutButtons.tsx
index 8a7ff794d..319779034 100644
--- a/packages/graph-editor/src/components/toolbar/layoutButtons.tsx
+++ b/packages/graph-editor/src/components/toolbar/layoutButtons.tsx
@@ -4,6 +4,7 @@ import { GraphPanel } from '../panels/graph/index.js';
import { Inputsheet } from '../panels/inputs/index.js';
import { Legend } from '../panels/legend/index.js';
import { LogsPanel } from '../panels/logs/index.js';
+import { NavigationPanel } from '../panels/navigation/index.js';
import { NodeSettingsPanel } from '../panels/nodeSettings/index.js';
import { OutputSheet } from '../panels/output/index.js';
import { Settings } from '../panels/settings/index.js';
@@ -55,6 +56,11 @@ export const layoutButtons = {
title: 'Nodes',
content: ,
},
+ navigationPanel: {
+ id: 'navigationPanel',
+ title: 'Navigation',
+ content: ,
+ },
};
export type LayoutButtons = keyof typeof layoutButtons;
diff --git a/packages/graph-editor/src/data/version.ts b/packages/graph-editor/src/data/version.ts
index 065128b05..23911495f 100644
--- a/packages/graph-editor/src/data/version.ts
+++ b/packages/graph-editor/src/data/version.ts
@@ -1 +1 @@
-export const version = '4.3.9';
+export const version = '4.3.6';
diff --git a/packages/graph-editor/src/editor/graphEditor.tsx b/packages/graph-editor/src/editor/graphEditor.tsx
index c9ebe960a..34c49b6db 100644
--- a/packages/graph-editor/src/editor/graphEditor.tsx
+++ b/packages/graph-editor/src/editor/graphEditor.tsx
@@ -1,4 +1,6 @@
import { EditorApp } from './graph.js';
+import { ErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js';
import { GraphEditorProps, ImperativeEditorRef } from './editorTypes.js';
import { ReactFlowProvider } from 'reactflow';
import React from 'react';
@@ -16,3 +18,15 @@ export const GraphEditor = React.forwardRef<
);
});
+
+// HACK: Workaround for circular dependency not allowed for nextjs
+// E.g.: when trying to create a new graph editor instance as a tab
+if (typeof window !== 'undefined') {
+ window['newGraphEditor'] = function (ref, id) {
+ return (
+ }>
+
+
+ );
+ };
+}
diff --git a/packages/graph-editor/src/hooks/useSubgraphExplorerCallback.ts b/packages/graph-editor/src/hooks/useSubgraphExplorerCallback.ts
new file mode 100644
index 000000000..bdb25ce5d
--- /dev/null
+++ b/packages/graph-editor/src/hooks/useSubgraphExplorerCallback.ts
@@ -0,0 +1,47 @@
+import { ImperativeEditorRef } from '../index.js';
+import { title as annotatedTitle } from '@/annotations/index.js';
+import { dockerSelector } from '@/redux/selectors/refs.js';
+import { useCallback } from 'react';
+import { useSelector } from 'react-redux';
+
+export const useSubgraphExplorerCallback = (node) => {
+ const dockerRef = useSelector(dockerSelector);
+
+ const callback = useCallback(() => {
+ if (!dockerRef?.current) {
+ return;
+ }
+
+ let oneShot = false;
+ const innerGraph = node['_innerGraph'];
+ const graphId = innerGraph.annotations['engine.id'];
+ const title =
+ node.annotations[annotatedTitle] ||
+ innerGraph.annotations['engine.title'] ||
+ 'Subgraph';
+ const existing = dockerRef.current.find(graphId);
+
+ if (!existing) {
+ const ref = (o: ImperativeEditorRef) => {
+ if (o && !oneShot) {
+ o.load(innerGraph);
+ oneShot = true;
+ }
+ };
+
+ const newTab = {
+ cached: true,
+ closable: true,
+ id: graphId,
+ group: 'graph',
+ title,
+ content: window && window['newGraphEditor'](ref, graphId),
+ };
+ dockerRef.current.dockMove(newTab, 'graphs', 'middle');
+ } else {
+ dockerRef.current.updateTab(graphId, null, true);
+ }
+ }, [dockerRef, node['_innerGraph'], node.annotations]);
+
+ return callback;
+};
diff --git a/packages/graph-editor/src/redux/selectors/graph.ts b/packages/graph-editor/src/redux/selectors/graph.ts
index 94a70e03c..945019ecb 100644
--- a/packages/graph-editor/src/redux/selectors/graph.ts
+++ b/packages/graph-editor/src/redux/selectors/graph.ts
@@ -1,3 +1,4 @@
+import { Graph, Node } from '@tokens-studio/graph-engine';
import { MAIN_GRAPH_ID } from '@/constants.js';
import { createSelector } from 'reselect';
import { graph } from './roots.js';
@@ -18,6 +19,36 @@ export const mainGraphSelector = createSelector(
(state) => state.panels[MAIN_GRAPH_ID],
);
+export type TreeNode = {
+ node: Node;
+ depth: number;
+};
+
+const collectNodes = function (
+ graph: Graph,
+ coll: Record = {},
+ depth = 1,
+): Record {
+ for (const id in graph.nodes) {
+ const node: Node = graph.nodes[id];
+ const innerGraph = node['_innerGraph'];
+ coll[id] = { node, depth } as TreeNode;
+
+ if (innerGraph) {
+ collectNodes(innerGraph, coll, ++depth);
+ }
+ }
+ return coll;
+};
+
+export const graphNodesSelector = createSelector(graph, (state) => {
+ const graph = state.panels[MAIN_GRAPH_ID]?.graph;
+
+ if (!graph) return;
+
+ return collectNodes(graph);
+});
+
export const graphEditorSelector = createSelector(
graph,
(state) => state.currentPanel?.ref,
diff --git a/packages/graph-editor/src/registry/specifics.tsx b/packages/graph-editor/src/registry/specifics.tsx
index efb719c7c..3b465ad08 100644
--- a/packages/graph-editor/src/registry/specifics.tsx
+++ b/packages/graph-editor/src/registry/specifics.tsx
@@ -16,31 +16,31 @@ import { useSelector } from 'react-redux';
import Eye from '@tokens-studio/icons/Eye.js';
import React, { useCallback } from 'react';
-const SubgraphExplorer = ({ node }) => {
+export const useSubgraphExplorerCallback = (node) => {
const dockerRef = useSelector(dockerSelector);
- const onToggle = useCallback(() => {
+
+ const callback = useCallback(() => {
if (!dockerRef?.current) {
return;
}
let oneShot = false;
- const innerGraph = node._innerGraph;
+ const innerGraph = node['_innerGraph'];
const graphId = innerGraph.annotations['engine.id'];
const title =
node.annotations[annotatedTitle] ||
innerGraph.annotations['engine.title'] ||
'Subgraph';
- //Find the container
const existing = dockerRef.current.find(graphId);
- const ref = (o: ImperativeEditorRef) => {
- if (o && !oneShot) {
- o.load(innerGraph);
- oneShot = true;
- }
- };
-
if (!existing) {
+ const ref = (o: ImperativeEditorRef) => {
+ if (o && !oneShot) {
+ o.load(innerGraph);
+ oneShot = true;
+ }
+ };
+
const newTab = {
cached: true,
closable: true,
@@ -53,15 +53,20 @@ const SubgraphExplorer = ({ node }) => {
),
};
-
dockerRef.current.dockMove(newTab, 'graphs', 'middle');
} else {
dockerRef.current.updateTab(graphId, null, true);
}
- }, [dockerRef, node._innerGraph, node.annotations]);
+ }, [dockerRef, node['_innerGraph'], node.annotations]);
+
+ return callback;
+};
+
+const SubgraphExplorer = ({ node }: { node: Node }) => {
+ const onClick = useSubgraphExplorerCallback(node);
return (
- } onClick={onToggle}>
+ } onClick={onClick}>
Subgraph Explorer
);