From 8c6deaae5461393b7f5d1c6c60bea426f7ac1a7c Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Wed, 4 Oct 2023 19:59:35 +0400 Subject: [PATCH] account for node configuration in graph editor The objective of this commit is to correctly render graphs like these: https://github.com/bhouston/behave-graph/blob/3e6568caa9a46a6f6aee27b0192a478f30c9c541/graphs/core/flow/WaitAll.json When loading JSON, a node may have arbitrary configuration parameters, that previously got ignored (such as number of sequence outputs). This commit tracks configuration for each rendered node, and accounts for it in all calculations (such as adding new edge). In order to do this, instead of NodeSpecJSON (object) we now pass a NodeSpecGenerator (class) which generates node specs on demand based on passed configuration (and also memoizes, not sure if that's necessary). New nodes will still be generated with no configuration, and no UI work to support it has been done. --- apps/export-node-spec/src/index.ts | 4 +- .../src/Graphs/IO/writeNodeSpecsToJSON.ts | 98 ++++++++++--------- packages/flow/src/components/Controls.tsx | 13 +-- packages/flow/src/components/Flow.tsx | 12 +-- packages/flow/src/components/InputSocket.tsx | 9 +- packages/flow/src/components/Node.tsx | 11 ++- packages/flow/src/components/OutputSocket.tsx | 9 +- .../flow/src/components/modals/SaveModal.tsx | 9 +- packages/flow/src/hooks/useBehaveGraphFlow.ts | 15 +-- packages/flow/src/hooks/useChangeNodeData.ts | 7 +- .../flow/src/hooks/useCustomNodeTypes.tsx | 24 ++--- packages/flow/src/hooks/useFlowHandlers.ts | 24 ++--- .../flow/src/hooks/useNodeSpecGenerator.ts | 50 ++++++++++ packages/flow/src/hooks/useNodeSpecJson.ts | 20 ---- packages/flow/src/index.ts | 1 - .../flow/src/transformers/behaveToFlow.ts | 15 ++- .../src/transformers/flowToBehave.test.ts | 6 +- .../flow/src/transformers/flowToBehave.ts | 19 ++-- packages/flow/src/util/calculateNewEdge.ts | 10 +- packages/flow/src/util/getPickerFilters.ts | 9 +- .../util/getSocketsByNodeTypeAndHandleType.ts | 12 ++- packages/flow/src/util/isValidConnection.ts | 13 ++- .../generate-pages-from-registry.ts | 7 +- 23 files changed, 232 insertions(+), 165 deletions(-) create mode 100644 packages/flow/src/hooks/useNodeSpecGenerator.ts delete mode 100644 packages/flow/src/hooks/useNodeSpecJson.ts diff --git a/apps/export-node-spec/src/index.ts b/apps/export-node-spec/src/index.ts index 6028d648..6c92cb5d 100644 --- a/apps/export-node-spec/src/index.ts +++ b/apps/export-node-spec/src/index.ts @@ -6,7 +6,7 @@ import { ManualLifecycleEventEmitter, registerCoreProfile, validateNodeRegistry, - writeNodeSpecsToJSON + writeDefaultNodeSpecsToJSON, } from '@behave-graph/core'; import { DummyScene, registerSceneProfile } from '@behave-graph/scene'; import { program } from 'commander'; @@ -59,7 +59,7 @@ export const main = async () => { return; } - const nodeSpecJson = writeNodeSpecsToJSON(registry); + const nodeSpecJson = writeDefaultNodeSpecsToJSON(registry); nodeSpecJson.sort((a, b) => a.type.localeCompare(b.type)); const jsonOutput = JSON.stringify(nodeSpecJson, null, ' '); if (programOptions.csv) { diff --git a/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts b/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts index 90c3292a..a6245923 100644 --- a/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts +++ b/packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts @@ -2,6 +2,7 @@ import { NodeCategory } from '../../Nodes/NodeDefinitions.js'; import { IRegistry } from '../../Registry.js'; import { Choices } from '../../Sockets/Socket.js'; import { createNode, makeGraphApi } from '../Graph.js'; +import { NodeConfigurationJSON } from './GraphJSON.js'; import { ChoiceJSON, InputSocketSpecJSON, @@ -16,64 +17,69 @@ function toChoices(valueChoices: Choices | undefined): ChoiceJSON | undefined { }); } -export function writeNodeSpecsToJSON(registry: IRegistry): NodeSpecJSON[] { - const nodeSpecsJSON: NodeSpecJSON[] = []; - - // const graph = new Graph(registry); - +// create JSON specs for a single node based on given configuration +export function writeNodeSpecToJSON(registry: IRegistry, nodeTypeName: string, configuration: NodeConfigurationJSON): NodeSpecJSON { const graph = makeGraphApi({ ...registry, customEvents: {}, variables: {} }); - Object.keys(registry.nodes).forEach((nodeTypeName) => { - const node = createNode({ - graph, - registry, - nodeTypeName - }); + const node = createNode({ + graph, + registry, + nodeTypeName, + nodeConfiguration: configuration, + }); + + const nodeSpecJSON: NodeSpecJSON = { + type: nodeTypeName, + category: node.description.category as NodeCategory, + label: node.description.label, + inputs: [], + outputs: [], + configuration: [] + }; + + node.inputs.forEach((inputSocket) => { + const valueType = + inputSocket.valueTypeName === 'flow' + ? undefined + : registry.values[inputSocket.valueTypeName]; - const nodeSpecJSON: NodeSpecJSON = { - type: nodeTypeName, - category: node.description.category as NodeCategory, - label: node.description.label, - inputs: [], - outputs: [], - configuration: [] + let defaultValue = inputSocket.value; + if (valueType !== undefined) { + defaultValue = valueType.serialize(defaultValue); + } + if (defaultValue === undefined && valueType !== undefined) { + defaultValue = valueType.serialize(valueType.creator()); + } + const socketSpecJSON: InputSocketSpecJSON = { + name: inputSocket.name, + valueType: inputSocket.valueTypeName, + defaultValue, + choices: toChoices(inputSocket.valueChoices) }; + nodeSpecJSON.inputs.push(socketSpecJSON); + }); - node.inputs.forEach((inputSocket) => { - const valueType = - inputSocket.valueTypeName === 'flow' - ? undefined - : registry.values[inputSocket.valueTypeName]; + node.outputs.forEach((outputSocket) => { + const socketSpecJSON: OutputSocketSpecJSON = { + name: outputSocket.name, + valueType: outputSocket.valueTypeName + }; + nodeSpecJSON.outputs.push(socketSpecJSON); + }); - let defaultValue = inputSocket.value; - if (valueType !== undefined) { - defaultValue = valueType.serialize(defaultValue); - } - if (defaultValue === undefined && valueType !== undefined) { - defaultValue = valueType.serialize(valueType.creator()); - } - const socketSpecJSON: InputSocketSpecJSON = { - name: inputSocket.name, - valueType: inputSocket.valueTypeName, - defaultValue, - choices: toChoices(inputSocket.valueChoices) - }; - nodeSpecJSON.inputs.push(socketSpecJSON); - }); + return nodeSpecJSON; +} - node.outputs.forEach((outputSocket) => { - const socketSpecJSON: OutputSocketSpecJSON = { - name: outputSocket.name, - valueType: outputSocket.valueTypeName - }; - nodeSpecJSON.outputs.push(socketSpecJSON); - }); +// create JSON specs for all nodes with empty configuration +export function writeDefaultNodeSpecsToJSON(registry: IRegistry): NodeSpecJSON[] { + const nodeSpecsJSON: NodeSpecJSON[] = []; - nodeSpecsJSON.push(nodeSpecJSON); + Object.keys(registry.nodes).forEach((nodeTypeName) => { + nodeSpecsJSON.push(writeNodeSpecToJSON(registry, nodeTypeName, {})); }); return nodeSpecsJSON; diff --git a/packages/flow/src/components/Controls.tsx b/packages/flow/src/components/Controls.tsx index 7c8a70bd..b2a7c786 100644 --- a/packages/flow/src/components/Controls.tsx +++ b/packages/flow/src/components/Controls.tsx @@ -1,4 +1,4 @@ -import { GraphJSON, NodeSpecJSON } from '@behave-graph/core'; +import { GraphJSON } from '@behave-graph/core'; import { faDownload, faPause, @@ -16,13 +16,14 @@ import { ClearModal } from './modals/ClearModal.js'; import { HelpModal } from './modals/HelpModal.js'; import { Examples, LoadModal } from './modals/LoadModal.js'; import { SaveModal } from './modals/SaveModal.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export type CustomControlsProps = { playing: boolean; togglePlay: () => void; setBehaviorGraph: (value: GraphJSON) => void; examples: Examples; - specJson: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }; export const CustomControls: React.FC = ({ @@ -30,13 +31,13 @@ export const CustomControls: React.FC = ({ togglePlay, setBehaviorGraph, examples, - specJson + specGenerator }: { playing: boolean; togglePlay: () => void; setBehaviorGraph: (value: GraphJSON) => void; examples: Examples; - specJson: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }) => { const [loadModalOpen, setLoadModalOpen] = useState(false); const [saveModalOpen, setSaveModalOpen] = useState(false); @@ -68,10 +69,10 @@ export const CustomControls: React.FC = ({ setBehaviorGraph={setBehaviorGraph} examples={examples} /> - {specJson && ( + {specGenerator && ( setSaveModalOpen(false)} /> )} diff --git a/packages/flow/src/components/Flow.tsx b/packages/flow/src/components/Flow.tsx index 472ec4a0..b6e3b468 100644 --- a/packages/flow/src/components/Flow.tsx +++ b/packages/flow/src/components/Flow.tsx @@ -5,7 +5,7 @@ import { Background, BackgroundVariant, ReactFlow } from 'reactflow'; import { useBehaveGraphFlow } from '../hooks/useBehaveGraphFlow.js'; import { useFlowHandlers } from '../hooks/useFlowHandlers.js'; import { useGraphRunner } from '../hooks/useGraphRunner.js'; -import { useNodeSpecJson } from '../hooks/useNodeSpecJson.js'; +import { useNodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; import CustomControls from './Controls.js'; import { Examples } from './modals/LoadModal.js'; import { NodePicker } from './NodePicker.js'; @@ -21,7 +21,7 @@ export const Flow: React.FC = ({ registry, examples }) => { - const specJson = useNodeSpecJson(registry); + const specGenerator = useNodeSpecGenerator(registry); const { nodes, @@ -33,7 +33,7 @@ export const Flow: React.FC = ({ nodeTypes } = useBehaveGraphFlow({ initialGraphJson: graph, - specJson + specGenerator }); const { @@ -51,7 +51,7 @@ export const Flow: React.FC = ({ nodes, onEdgesChange, onNodesChange, - specJSON: specJson + specGenerator, }); const { togglePlay, playing } = useGraphRunner({ @@ -81,7 +81,7 @@ export const Flow: React.FC = ({ togglePlay={togglePlay} setBehaviorGraph={setGraphJson} examples={examples} - specJson={specJson} + specGenerator={specGenerator} /> = ({ filters={nodePickFilters} onPickNode={handleAddNode} onClose={closeNodePicker} - specJSON={specJson} + specJSON={specGenerator?.getAllNodeSpecs()} /> )} diff --git a/packages/flow/src/components/InputSocket.tsx b/packages/flow/src/components/InputSocket.tsx index 70ee9d01..245a0b2e 100644 --- a/packages/flow/src/components/InputSocket.tsx +++ b/packages/flow/src/components/InputSocket.tsx @@ -1,4 +1,4 @@ -import { InputSocketSpecJSON, NodeSpecJSON } from '@behave-graph/core'; +import { InputSocketSpecJSON } from '@behave-graph/core'; import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cx from 'classnames'; @@ -8,12 +8,13 @@ import { Connection, Handle, Position, useReactFlow } from 'reactflow'; import { colors, valueTypeColorMap } from '../util/colors.js'; import { isValidConnection } from '../util/isValidConnection.js'; import { AutoSizeInput } from './AutoSizeInput.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export type InputSocketProps = { connected: boolean; value: any | undefined; onChange: (key: string, value: any) => void; - specJSON: NodeSpecJSON[]; + specGenerator: NodeSpecGenerator; } & InputSocketSpecJSON; const InputFieldForValue = ({ @@ -95,7 +96,7 @@ const InputFieldForValue = ({ const InputSocket: React.FC = ({ connected, - specJSON, + specGenerator, ...rest }) => { const { value, name, valueType, defaultValue, choices } = rest; @@ -128,7 +129,7 @@ const InputSocket: React.FC = ({ position={Position.Left} className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')} isValidConnection={(connection: Connection) => - isValidConnection(connection, instance, specJSON) + isValidConnection(connection, instance, specGenerator) } /> diff --git a/packages/flow/src/components/Node.tsx b/packages/flow/src/components/Node.tsx index 1fbf849f..4b27d245 100644 --- a/packages/flow/src/components/Node.tsx +++ b/packages/flow/src/components/Node.tsx @@ -7,10 +7,11 @@ import { isHandleConnected } from '../util/isHandleConnected.js'; import InputSocket from './InputSocket.js'; import NodeContainer from './NodeContainer.js'; import OutputSocket from './OutputSocket.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; type NodeProps = FlowNodeProps & { spec: NodeSpecJSON; - allSpecs: NodeSpecJSON[]; + specGenerator: NodeSpecGenerator; }; const getPairs = (arr1: T[], arr2: U[]) => { @@ -28,7 +29,7 @@ export const Node: React.FC = ({ data, spec, selected, - allSpecs + specGenerator, }: NodeProps) => { const edges = useEdges(); const handleChange = useChangeNodeData(id); @@ -48,8 +49,8 @@ export const Node: React.FC = ({ {input && ( @@ -57,7 +58,7 @@ export const Node: React.FC = ({ {output && ( )} diff --git a/packages/flow/src/components/OutputSocket.tsx b/packages/flow/src/components/OutputSocket.tsx index 03f97914..f4bcc82a 100644 --- a/packages/flow/src/components/OutputSocket.tsx +++ b/packages/flow/src/components/OutputSocket.tsx @@ -1,4 +1,4 @@ -import { NodeSpecJSON, OutputSocketSpecJSON } from '@behave-graph/core'; +import { OutputSocketSpecJSON } from '@behave-graph/core'; import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cx from 'classnames'; @@ -7,14 +7,15 @@ import { Connection, Handle, Position, useReactFlow } from 'reactflow'; import { colors, valueTypeColorMap } from '../util/colors.js'; import { isValidConnection } from '../util/isValidConnection.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export type OutputSocketProps = { connected: boolean; - specJSON: NodeSpecJSON[]; + specGenerator: NodeSpecGenerator; } & OutputSocketSpecJSON; export default function OutputSocket({ - specJSON, + specGenerator, connected, valueType, name @@ -47,7 +48,7 @@ export default function OutputSocket({ position={Position.Right} className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')} isValidConnection={(connection: Connection) => - isValidConnection(connection, instance, specJSON) + isValidConnection(connection, instance, specGenerator) } /> diff --git a/packages/flow/src/components/modals/SaveModal.tsx b/packages/flow/src/components/modals/SaveModal.tsx index fc0f8b18..be00a30f 100644 --- a/packages/flow/src/components/modals/SaveModal.tsx +++ b/packages/flow/src/components/modals/SaveModal.tsx @@ -5,17 +5,18 @@ import { useEdges, useNodes } from 'reactflow'; import { flowToBehave } from '../../transformers/flowToBehave.js'; import { Modal } from './Modal.js'; +import { NodeSpecGenerator } from '../../hooks/useNodeSpecGenerator.js'; export type SaveModalProps = { open?: boolean; onClose: () => void; - specJson: NodeSpecJSON[]; + specGenerator: NodeSpecGenerator; }; export const SaveModal: React.FC = ({ open = false, onClose, - specJson + specGenerator }) => { const ref = useRef(null); const [copied, setCopied] = useState(false); @@ -24,8 +25,8 @@ export const SaveModal: React.FC = ({ const nodes = useNodes(); const flow = useMemo( - () => flowToBehave(nodes, edges, specJson), - [nodes, edges, specJson] + () => flowToBehave(nodes, edges, specGenerator), + [nodes, edges, specGenerator] ); const jsonString = JSON.stringify(flow, null, 2); diff --git a/packages/flow/src/hooks/useBehaveGraphFlow.ts b/packages/flow/src/hooks/useBehaveGraphFlow.ts index 55132d23..d068073f 100644 --- a/packages/flow/src/hooks/useBehaveGraphFlow.ts +++ b/packages/flow/src/hooks/useBehaveGraphFlow.ts @@ -1,4 +1,4 @@ -import { GraphJSON, NodeSpecJSON } from '@behave-graph/core'; +import { GraphJSON } from '@behave-graph/core'; import { useCallback, useEffect, useState } from 'react'; import { useEdgesState, useNodesState } from 'reactflow'; @@ -7,6 +7,7 @@ import { flowToBehave } from '../transformers/flowToBehave.js'; import { autoLayout } from '../util/autoLayout.js'; import { hasPositionMetaData } from '../util/hasPositionMetaData.js'; import { useCustomNodeTypes } from './useCustomNodeTypes.js'; +import { NodeSpecGenerator } from './useNodeSpecGenerator.js'; export const fetchBehaviorGraphJson = async (url: string) => // eslint-disable-next-line unicorn/no-await-expression-member @@ -21,10 +22,10 @@ export const fetchBehaviorGraphJson = async (url: string) => */ export const useBehaveGraphFlow = ({ initialGraphJson, - specJson + specGenerator }: { initialGraphJson: GraphJSON; - specJson: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }) => { const [graphJson, setStoredGraphJson] = useState(); const [nodes, setNodes, onNodesChange] = useNodesState([]); @@ -53,14 +54,14 @@ export const useBehaveGraphFlow = ({ }, [initialGraphJson, setGraphJson]); useEffect(() => { - if (!specJson) return; + if (!specGenerator) return; // when nodes and edges are updated, update the graph json with the flow to behave behavior - const graphJson = flowToBehave(nodes, edges, specJson); + const graphJson = flowToBehave(nodes, edges, specGenerator); setStoredGraphJson(graphJson); - }, [nodes, edges, specJson]); + }, [nodes, edges, specGenerator]); const nodeTypes = useCustomNodeTypes({ - specJson + specGenerator }); return { diff --git a/packages/flow/src/hooks/useChangeNodeData.ts b/packages/flow/src/hooks/useChangeNodeData.ts index cee373f3..15bfc28b 100644 --- a/packages/flow/src/hooks/useChangeNodeData.ts +++ b/packages/flow/src/hooks/useChangeNodeData.ts @@ -13,8 +13,11 @@ export const useChangeNodeData = (id: string) => { ...n, data: { ...n.data, - [key]: value - } + values: { + ...n.data.values, + [key]: value + }, + }, }; }) ); diff --git a/packages/flow/src/hooks/useCustomNodeTypes.tsx b/packages/flow/src/hooks/useCustomNodeTypes.tsx index dbeb795b..18aa30d9 100644 --- a/packages/flow/src/hooks/useCustomNodeTypes.tsx +++ b/packages/flow/src/hooks/useCustomNodeTypes.tsx @@ -1,31 +1,31 @@ -import { NodeSpecJSON } from '@behave-graph/core'; -import React from 'react'; import { useEffect, useState } from 'react'; import { NodeTypes } from 'reactflow'; import { Node } from '../components/Node.js'; +import { NodeSpecGenerator } from './useNodeSpecGenerator.js'; -const getCustomNodeTypes = (allSpecs: NodeSpecJSON[]) => { - return allSpecs.reduce((nodes: NodeTypes, node) => { - nodes[node.type] = (props) => ( - - ); +const getCustomNodeTypes = (specGenerator: NodeSpecGenerator) => { + return specGenerator.getNodeTypes().reduce((nodes: NodeTypes, nodeType) => { + nodes[nodeType] = (props) => { + let spec = specGenerator.getNodeSpec(nodeType, props.data.configuration); + return ; + }; return nodes; }, {}); }; export const useCustomNodeTypes = ({ - specJson + specGenerator }: { - specJson: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }) => { const [customNodeTypes, setCustomNodeTypes] = useState(); useEffect(() => { - if (!specJson) return; - const customNodeTypes = getCustomNodeTypes(specJson); + if (!specGenerator) return; + const customNodeTypes = getCustomNodeTypes(specGenerator); setCustomNodeTypes(customNodeTypes); - }, [specJson]); + }, [specGenerator]); return customNodeTypes; }; diff --git a/packages/flow/src/hooks/useFlowHandlers.ts b/packages/flow/src/hooks/useFlowHandlers.ts index 3e89a691..2e456992 100644 --- a/packages/flow/src/hooks/useFlowHandlers.ts +++ b/packages/flow/src/hooks/useFlowHandlers.ts @@ -1,4 +1,3 @@ -import { NodeSpecJSON } from '@behave-graph/core'; import { MouseEvent as ReactMouseEvent, useCallback, @@ -11,25 +10,26 @@ import { v4 as uuidv4 } from 'uuid'; import { calculateNewEdge } from '../util/calculateNewEdge.js'; import { getNodePickerFilters } from '../util/getPickerFilters.js'; import { useBehaveGraphFlow } from './useBehaveGraphFlow.js'; +import { NodeSpecGenerator } from './useNodeSpecGenerator.js'; type BehaveGraphFlow = ReturnType; const useNodePickFilters = ({ nodes, lastConnectStart, - specJSON + specGenerator, }: { nodes: Node[]; lastConnectStart: OnConnectStartParams | undefined; - specJSON: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }) => { const [nodePickFilters, setNodePickFilters] = useState( - getNodePickerFilters(nodes, lastConnectStart, specJSON) + getNodePickerFilters(nodes, lastConnectStart, specGenerator) ); useEffect(() => { - setNodePickFilters(getNodePickerFilters(nodes, lastConnectStart, specJSON)); - }, [nodes, lastConnectStart, specJSON]); + setNodePickFilters(getNodePickerFilters(nodes, lastConnectStart, specGenerator)); + }, [nodes, lastConnectStart, specGenerator]); return nodePickFilters; }; @@ -38,10 +38,10 @@ export const useFlowHandlers = ({ onEdgesChange, onNodesChange, nodes, - specJSON + specGenerator, }: Pick & { nodes: Node[]; - specJSON: NodeSpecJSON[] | undefined; + specGenerator: NodeSpecGenerator | undefined; }) => { const [lastConnectStart, setLastConnectStart] = useState(); @@ -98,7 +98,7 @@ export const useFlowHandlers = ({ (node) => node.id === lastConnectStart.nodeId ); if (originNode === undefined) return; - if (!specJSON) return; + if (!specGenerator) return; onEdgesChange([ { type: 'add', @@ -107,7 +107,7 @@ export const useFlowHandlers = ({ nodeType, newNode.id, lastConnectStart, - specJSON + specGenerator, ) } ]); @@ -118,7 +118,7 @@ export const useFlowHandlers = ({ nodes, onEdgesChange, onNodesChange, - specJSON + specGenerator, ] ); @@ -151,7 +151,7 @@ export const useFlowHandlers = ({ const nodePickFilters = useNodePickFilters({ nodes, lastConnectStart, - specJSON + specGenerator, }); return { diff --git a/packages/flow/src/hooks/useNodeSpecGenerator.ts b/packages/flow/src/hooks/useNodeSpecGenerator.ts new file mode 100644 index 00000000..f1c64dbb --- /dev/null +++ b/packages/flow/src/hooks/useNodeSpecGenerator.ts @@ -0,0 +1,50 @@ +// Generates node specs based on provided configuration, +// and caches the results. + +import { + IRegistry, + NodeConfigurationJSON, + NodeSpecJSON, + writeDefaultNodeSpecsToJSON, + writeNodeSpecToJSON, +} from '@behave-graph/core'; +import { useEffect, useState } from 'react'; + +export class NodeSpecGenerator { + private specsWithoutConfig?: NodeSpecJSON[]; + private specsCache: { [cacheKey: string]: NodeSpecJSON } = {}; + + constructor(private registry: IRegistry) {} + + getNodeTypes(): string[] { + return Object.keys(this.registry.nodes); + } + + getNodeSpec(nodeTypeName: string, configuration: NodeConfigurationJSON): NodeSpecJSON { + let cacheKey = nodeTypeName + '\x01' + JSON.stringify(configuration); + + if (!this.specsCache[cacheKey]) { + this.specsCache[cacheKey] = writeNodeSpecToJSON(this.registry, nodeTypeName, configuration); + } + + return this.specsCache[cacheKey]; + } + + getAllNodeSpecs(): NodeSpecJSON[] { + if (!this.specsWithoutConfig) { + this.specsWithoutConfig = writeDefaultNodeSpecsToJSON(this.registry); + } + + return this.specsWithoutConfig; + } +} + +export const useNodeSpecGenerator = (registry: IRegistry) => { + const [specGenerator, setSpecGenerator] = useState(); + + useEffect(() => { + setSpecGenerator(new NodeSpecGenerator(registry)); + }, [registry.nodes, registry.values, registry.dependencies]); + + return specGenerator; +}; diff --git a/packages/flow/src/hooks/useNodeSpecJson.ts b/packages/flow/src/hooks/useNodeSpecJson.ts deleted file mode 100644 index 671210d9..00000000 --- a/packages/flow/src/hooks/useNodeSpecJson.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - IRegistry, - NodeSpecJSON, - writeNodeSpecsToJSON -} from '@behave-graph/core'; -import { useEffect, useState } from 'react'; - -export const useNodeSpecJson = (registry: IRegistry) => { - const [specJson, setSpecJson] = useState(); - - useEffect(() => { - if (!registry.nodes || !registry.values || !registry.dependencies) { - setSpecJson(undefined); - return; - } - setSpecJson(writeNodeSpecsToJSON(registry)); - }, [registry.nodes, registry.values, registry.dependencies]); - - return specJson; -}; diff --git a/packages/flow/src/index.ts b/packages/flow/src/index.ts index 5cb4cdd9..4a77606c 100644 --- a/packages/flow/src/index.ts +++ b/packages/flow/src/index.ts @@ -19,7 +19,6 @@ export * from './hooks/useOnPressKey.js'; export * from './hooks/useFlowHandlers.js'; export * from './hooks/useGraphRunner.js'; export * from './hooks/useBehaveGraphFlow.js'; -export * from './hooks/useNodeSpecJson.js'; export * from './hooks/useCustomNodeTypes.js'; export * from './hooks/useMergeMap.js'; diff --git a/packages/flow/src/transformers/behaveToFlow.ts b/packages/flow/src/transformers/behaveToFlow.ts index 583c3d5d..5a804bdf 100644 --- a/packages/flow/src/transformers/behaveToFlow.ts +++ b/packages/flow/src/transformers/behaveToFlow.ts @@ -1,4 +1,4 @@ -import { GraphJSON } from '@behave-graph/core'; +import { GraphJSON, NodeConfigurationJSON } from '@behave-graph/core'; import { Edge, Node } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; @@ -18,11 +18,20 @@ export const behaveToFlow = (graph: GraphJSON): [Node[], Edge[]] => { ? Number(nodeJSON.metadata?.positionY) : 0 }, - data: {} as { [key: string]: any } + data: { + configuration: {} as NodeConfigurationJSON, + values: {} as { [key: string]: any }, + }, }; nodes.push(node); + if (nodeJSON.configuration) { + for (const [inputKey, input] of Object.entries(nodeJSON.configuration)) { + node.data.configuration[inputKey] = input; + } + } + if (nodeJSON.parameters) { for (const [inputKey, input] of Object.entries(nodeJSON.parameters)) { if ('link' in input && input.link !== undefined) { @@ -35,7 +44,7 @@ export const behaveToFlow = (graph: GraphJSON): [Node[], Edge[]] => { }); } if ('value' in input) { - node.data[inputKey] = input.value; + node.data.values[inputKey] = input.value; } } } diff --git a/packages/flow/src/transformers/flowToBehave.test.ts b/packages/flow/src/transformers/flowToBehave.test.ts index 3b7b3458..5ec59207 100644 --- a/packages/flow/src/transformers/flowToBehave.test.ts +++ b/packages/flow/src/transformers/flowToBehave.test.ts @@ -1,12 +1,12 @@ import { getCoreRegistry, GraphJSON, - writeNodeSpecsToJSON } from '@behave-graph/core'; import rawFlowGraph from '../../../../graphs/react-flow/graph.json'; import { behaveToFlow } from './behaveToFlow.js'; import { flowToBehave } from './flowToBehave.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator'; const flowGraph = rawFlowGraph as GraphJSON; @@ -14,7 +14,7 @@ const [nodes, edges] = behaveToFlow(flowGraph); it('transforms from flow to behave', () => { const registry = getCoreRegistry(); - const specJSON = writeNodeSpecsToJSON(registry); - const output = flowToBehave(nodes, edges, specJSON); + const specGenerator = new NodeSpecGenerator(registry); + const output = flowToBehave(nodes, edges, specGenerator); expect(output).toEqual(flowGraph); }); diff --git a/packages/flow/src/transformers/flowToBehave.ts b/packages/flow/src/transformers/flowToBehave.ts index 60ca2a76..f6904c04 100644 --- a/packages/flow/src/transformers/flowToBehave.ts +++ b/packages/flow/src/transformers/flowToBehave.ts @@ -1,5 +1,6 @@ -import { GraphJSON, NodeJSON, NodeSpecJSON } from '@behave-graph/core'; +import { GraphJSON, NodeJSON, ValueJSON } from '@behave-graph/core'; import { Edge, Node } from 'reactflow'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; const isNullish = (value: any): value is null | undefined => value === undefined || value === null; @@ -7,17 +8,14 @@ const isNullish = (value: any): value is null | undefined => export const flowToBehave = ( nodes: Node[], edges: Edge[], - nodeSpecJSON: NodeSpecJSON[] + specGenerator: NodeSpecGenerator, ): GraphJSON => { const graph: GraphJSON = { nodes: [], variables: [], customEvents: [] }; nodes.forEach((node) => { if (node.type === undefined) return; - const nodeSpec = nodeSpecJSON.find( - (nodeSpec) => nodeSpec.type === node.type - ); - + const nodeSpec = specGenerator.getNodeSpec(node.type, node.data.configuration); if (nodeSpec === undefined) return; const behaveNode: NodeJSON = { @@ -29,7 +27,14 @@ export const flowToBehave = ( } }; - Object.entries(node.data).forEach(([key, value]) => { + Object.entries(node.data.configuration).forEach(([key, value]) => { + if (behaveNode.configuration === undefined) { + behaveNode.configuration = {}; + } + behaveNode.configuration[key] = value as ValueJSON; + }); + + Object.entries(node.data.values).forEach(([key, value]) => { if (behaveNode.parameters === undefined) { behaveNode.parameters = {}; } diff --git a/packages/flow/src/util/calculateNewEdge.ts b/packages/flow/src/util/calculateNewEdge.ts index 80cb71a6..e69ac7dc 100644 --- a/packages/flow/src/util/calculateNewEdge.ts +++ b/packages/flow/src/util/calculateNewEdge.ts @@ -1,19 +1,20 @@ -import { NodeSpecJSON } from '@behave-graph/core'; import { Node, OnConnectStartParams } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export const calculateNewEdge = ( originNode: Node, destinationNodeType: string, destinationNodeId: string, connection: OnConnectStartParams, - specJSON: NodeSpecJSON[] + specGenerator: NodeSpecGenerator, ) => { const sockets = getSocketsByNodeTypeAndHandleType( - specJSON, + specGenerator, originNode.type, + originNode.data.configuration, connection.handleType ); const originSocket = sockets?.find( @@ -21,8 +22,9 @@ export const calculateNewEdge = ( ); const newSockets = getSocketsByNodeTypeAndHandleType( - specJSON, + specGenerator, destinationNodeType, + {}, connection.handleType === 'source' ? 'target' : 'source' ); const newSocket = newSockets?.find( diff --git a/packages/flow/src/util/getPickerFilters.ts b/packages/flow/src/util/getPickerFilters.ts index a340bffc..25814b22 100644 --- a/packages/flow/src/util/getPickerFilters.ts +++ b/packages/flow/src/util/getPickerFilters.ts @@ -1,23 +1,24 @@ -import { NodeSpecJSON } from '@behave-graph/core'; import { Node, OnConnectStartParams } from 'reactflow'; import { NodePickerFilters } from '../components/NodePicker.js'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export const getNodePickerFilters = ( nodes: Node[], params: OnConnectStartParams | undefined, - specJSON: NodeSpecJSON[] | undefined + specGenerator: NodeSpecGenerator | undefined ): NodePickerFilters | undefined => { if (params === undefined) return; const originNode = nodes.find((node) => node.id === params.nodeId); if (originNode === undefined) return; - const sockets = specJSON + const sockets = specGenerator ? getSocketsByNodeTypeAndHandleType( - specJSON, + specGenerator, originNode.type, + originNode.data.configuration, params.handleType ) : undefined; diff --git a/packages/flow/src/util/getSocketsByNodeTypeAndHandleType.ts b/packages/flow/src/util/getSocketsByNodeTypeAndHandleType.ts index cdaf50f9..91a92b5f 100644 --- a/packages/flow/src/util/getSocketsByNodeTypeAndHandleType.ts +++ b/packages/flow/src/util/getSocketsByNodeTypeAndHandleType.ts @@ -1,11 +1,13 @@ -import { NodeSpecJSON } from '@behave-graph/core'; +import { NodeConfigurationJSON } from '@behave-graph/core'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export const getSocketsByNodeTypeAndHandleType = ( - nodes: NodeSpecJSON[], + specGenerator: NodeSpecGenerator, nodeType: string | undefined, - handleType: 'source' | 'target' | null + nodeConfiguration: NodeConfigurationJSON, + handleType: 'source' | 'target' | null, ) => { - const nodeSpec = nodes.find((node) => node.type === nodeType); - if (nodeSpec === undefined) return; + if (nodeType === undefined) return []; + const nodeSpec = specGenerator.getNodeSpec(nodeType, nodeConfiguration); return handleType === 'source' ? nodeSpec.outputs : nodeSpec.inputs; }; diff --git a/packages/flow/src/util/isValidConnection.ts b/packages/flow/src/util/isValidConnection.ts index e84d6114..66149983 100644 --- a/packages/flow/src/util/isValidConnection.ts +++ b/packages/flow/src/util/isValidConnection.ts @@ -3,11 +3,12 @@ import { Connection, ReactFlowInstance } from 'reactflow'; import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js'; import { isHandleConnected } from './isHandleConnected.js'; +import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js'; export const isValidConnection = ( connection: Connection, instance: ReactFlowInstance, - specJSON: NodeSpecJSON[] + specGenerator: NodeSpecGenerator, ) => { if (connection.source === null || connection.target === null) return false; @@ -18,9 +19,10 @@ export const isValidConnection = ( if (sourceNode === undefined || targetNode === undefined) return false; const sourceSockets = getSocketsByNodeTypeAndHandleType( - specJSON, + specGenerator, sourceNode.type, - 'source' + sourceNode.data.configuration, + 'source', ); const sourceSocket = sourceSockets?.find( @@ -28,9 +30,10 @@ export const isValidConnection = ( ); const targetSockets = getSocketsByNodeTypeAndHandleType( - specJSON, + specGenerator, targetNode.type, - 'target' + targetNode.data.configuration, + 'target', ); const targetSocket = targetSockets?.find( diff --git a/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts b/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts index 5ae331d5..a78c26e9 100644 --- a/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts +++ b/website/scripts/generate-dynamic-pages/generate-pages-from-registry.ts @@ -8,7 +8,7 @@ import { NodeSpecJSON, Registry, ValueType, - writeNodeSpecsToJSON + writeDefaultNodeSpecsToJSON, } from '@behave-graph/core'; // We need to transform directories to kebab case because otherwise Docusaurus won't generate the toString one import { kebab, pascal } from 'case'; @@ -142,7 +142,8 @@ const generateNodePages = ( }); }; -// First registry includes only the nodes for that specific profile, second registry includes all nodes required to run writeNodeSpecsToJSON +// First registry includes only the nodes for that specific profile, +// second registry includes all nodes required to run writeDefaultNodeSpecsToJSON export default ( registry: Registry, baseDir: string, @@ -152,7 +153,7 @@ export default ( const values = registry.values.getAll(); - const nodeSpecJson = writeNodeSpecsToJSON(functionalRegistry || registry); + const nodeSpecJson = writeDefaultNodeSpecsToJSON(functionalRegistry || registry); const graphApi = new Graph(registry).makeApi();