/**
* @file Advanced example demonstrating nested editors within nodes
*/
import * as React from "react";
import { NodeEditor } from "../../../../../index";
import { NodeCanvas } from "../../../../../components/canvas/NodeCanvas";
import { Minimap } from "../../../../../components/layers/Minimap";
import type { GridLayoutConfig, LayerDefinition } from "../../../../../types/panels";
import type { NodeEditorData } from "../../../../../types/core";
import { asNodeDefinition, type ExternalDataReference, type NodeDefinition } from "../../../../../types/NodeDefinition";
import { StandardNodeDefinition } from "../../../../../node-definitions/standard";
import { SubEditorHostProvider, type SubEditorOpenRequest } from "./SubEditorHostContext";
import { SubEditorNodeDefinition } from "./SubEditorNode";
import { SubEditorWindow } from "./SubEditorWindow";
import { advancedNestedInitialData, createDefaultSubEditorData } from "./initialData";
import { createSubEditorRefId, ensureSubEditorData } from "./subEditorDataStore";
import { isSubEditorNodeData } from "./types";
import classes from "./AdvancedNestedEditorExample.module.css";
type OpenEditorState = {
nodeId: string;
title: string;
externalRefId: string;
};
const mainGridConfig: GridLayoutConfig = {
areas: [["canvas"]],
rows: [{ size: "1fr" }],
columns: [{ size: "1fr" }],
gap: "0",
};
const baseLayers: LayerDefinition[] = [
{
id: "canvas",
component: ,
gridArea: "canvas",
zIndex: 0,
},
{
id: "minimap",
component: ,
positionMode: "absolute",
position: { right: 24, bottom: 24 },
zIndex: 50,
draggable: true,
width: 220,
height: 160,
},
];
const providedDefinitions: NodeDefinition[] = [
asNodeDefinition(SubEditorNodeDefinition),
asNodeDefinition(StandardNodeDefinition),
];
const buildExternalRefsFromData = (data: NodeEditorData): Record => {
const refs: Record = {};
for (const [nodeId, node] of Object.entries(data.nodes)) {
if (node.type !== "sub-editor" || !isSubEditorNodeData(node.data)) {
continue;
}
const refId = node.data.nestedEditorRefId || createSubEditorRefId(nodeId);
ensureSubEditorData(refId, () => createDefaultSubEditorData(nodeId));
refs[nodeId] = {
id: refId,
type: "sub-editor",
metadata: { namespace: nodeId },
};
}
return refs;
};
const initialExternalRefs = buildExternalRefsFromData(advancedNestedInitialData);
const buildInitialOpenEditors = (): Record => {
const automationRef = initialExternalRefs["sub-automation"];
if (!automationRef) {
return {};
}
return {
"sub-automation": {
nodeId: "sub-automation",
title: "Automation Flow",
externalRefId: automationRef.id,
},
};
};
export const AdvancedNestedEditorExample: React.FC = () => {
const [externalDataRefs, setExternalDataRefs] = React.useState>(() => ({
...initialExternalRefs,
}));
const [openEditors, setOpenEditors] = React.useState>(buildInitialOpenEditors);
const handleOpenSubEditor = React.useCallback((request: SubEditorOpenRequest) => {
setExternalDataRefs((prev) => {
const current = prev[request.nodeId];
if (current && current.id === request.externalRefId) {
return prev;
}
return {
...prev,
[request.nodeId]: {
id: request.externalRefId,
type: "sub-editor",
metadata: { namespace: request.nodeId },
},
};
});
setOpenEditors((prev) => {
const existing = prev[request.nodeId];
if (existing && existing.externalRefId === request.externalRefId && existing.title === request.title) {
return prev;
}
return {
...prev,
[request.nodeId]: {
nodeId: request.nodeId,
title: request.title,
externalRefId: request.externalRefId,
},
};
});
}, []);
const handleCloseSubEditor = React.useCallback((nodeId: string) => {
setOpenEditors((prev) => {
if (!prev[nodeId]) {
return prev;
}
const next = { ...prev };
delete next[nodeId];
return next;
});
}, []);
const handleDataChange = React.useCallback((data: NodeEditorData) => {
setExternalDataRefs((prev) => {
let mutated = false;
const next: Record = { ...prev };
const seen = new Set();
for (const [nodeId, node] of Object.entries(data.nodes)) {
if (node.type !== "sub-editor" || !isSubEditorNodeData(node.data)) {
if (next[nodeId]) {
mutated = true;
delete next[nodeId];
}
continue;
}
const refId = node.data.nestedEditorRefId || createSubEditorRefId(nodeId);
ensureSubEditorData(refId, () => createDefaultSubEditorData(nodeId));
const current = next[nodeId];
if (!current || current.id !== refId) {
next[nodeId] = {
id: refId,
type: "sub-editor",
metadata: { namespace: nodeId },
};
mutated = true;
}
seen.add(nodeId);
}
for (const key of Object.keys(prev)) {
if (!seen.has(key)) {
mutated = true;
delete next[key];
}
}
return mutated ? next : prev;
});
setOpenEditors((prev) => {
if (Object.keys(prev).length === 0) {
return prev;
}
let mutated = false;
const next: Record = {};
for (const key of Object.keys(prev)) {
const editor = prev[key];
const node = data.nodes[key];
if (!node) {
mutated = true;
continue;
}
const nodeData = node.data;
if (!isSubEditorNodeData(nodeData) || !nodeData.nestedEditorRefId) {
mutated = true;
continue;
}
const nextTitle = typeof nodeData.title === "string" ? nodeData.title : editor.title;
const refId = nodeData.nestedEditorRefId;
if (editor.title === nextTitle && editor.externalRefId === refId) {
next[key] = editor;
continue;
}
next[key] = { nodeId: key, title: nextTitle, externalRefId: refId };
mutated = true;
}
if (!mutated && Object.keys(next).length === Object.keys(prev).length) {
return prev;
}
return next;
});
}, []);
const openEditorList = React.useMemo(() => Object.values(openEditors), [openEditors]);
const subEditorLayers = React.useMemo(() => {
const layers: LayerDefinition[] = [];
openEditorList.forEach((editor, index) => {
const externalRef = externalDataRefs[editor.nodeId];
if (!externalRef) {
return;
}
layers.push({
id: `sub-editor-window-${editor.nodeId}`,
component: (
),
positionMode: "absolute",
position: {
right: 32 + index * 24,
top: 32 + index * 24,
},
width: 440,
height: 360,
draggable: true,
zIndex: 200 + index,
});
});
return layers;
}, [externalDataRefs, openEditorList, handleCloseSubEditor]);
const gridLayers = React.useMemo(() => [...baseLayers, ...subEditorLayers], [subEditorLayers]);
const hostValue = React.useMemo(
() => ({
openSubEditor: handleOpenSubEditor,
}),
[handleOpenSubEditor],
);
return (
);
};
export default AdvancedNestedEditorExample;
/*
debug-notes:
- Coordinated open editor state with SubEditorHostContext to prevent duplicate floating windows per node.
- Managed externalDataRefs map from NodeEditor data, enabling referential sub-editor updates without controlled data.
- Combined base canvas layers with dynamically generated LayerDefinition entries for each sub-editor window.
- Reviewed NodeEditor.tsx and GridLayout.tsx to confirm controlled-mode updates and draggable layer behaviour.
*/