Merge branch 'dev' into zustand/io/migration

This commit is contained in:
Lucas Oliveira 2024-01-31 11:54:25 +01:00
commit 89cb0f9e22
7 changed files with 51 additions and 176 deletions

View file

@ -57,7 +57,6 @@ export default function ParameterComponent({
proxy, proxy,
showNode, showNode,
index = "", index = "",
isMinimized,
}: ParameterComponentType): JSX.Element { }: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
@ -289,7 +288,7 @@ export default function ParameterComponent({
className={classNames( className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ", left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background", "h-3 w-3 rounded-full border-2 bg-background",
isMinimized ? "mt-0" : "" !showNode ? "mt-0" : ""
)} )}
style={{ style={{
borderColor: color, borderColor: color,

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow"; import { NodeToolbar } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent"; import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
@ -43,15 +43,12 @@ export default function GenericNode({
); );
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<boolean[] | []>([]); const [handles, setHandles] = useState<number>(0);
const [isMinimized, setIsMinimized] = useState<boolean>(false);
let numberOfInputs: boolean[] = [];
const updateNodeInternals = useUpdateNodeInternals();
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
function countHandles(): void { function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template) let count = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_") .filter((templateField) => templateField.charAt(0) !== "_")
.map((templateCamp) => { .map((templateCamp) => {
const { template } = data.node!; const { template } = data.node!;
@ -59,24 +56,20 @@ export default function GenericNode({
if (!template[templateCamp].show) return false; if (!template[templateCamp].show) return false;
switch (template[templateCamp].type) { switch (template[templateCamp].type) {
case "str": case "str":
return false;
case "bool": case "bool":
return false;
case "float": case "float":
return false;
case "code": case "code":
return false;
case "prompt": case "prompt":
return false;
case "file": case "file":
return false;
case "int": case "int":
return false; return false;
default: default:
return true; return true;
} }
}); })
setHandles(numberOfInputs); .reduce((total, value) => total + (value ? 1 : 0), 0);
setHandles(count);
} }
useEffect(() => { useEffect(() => {
@ -119,10 +112,6 @@ export default function GenericNode({
const nameEditable = data.node?.flow || data.type === "CustomComponent"; const nameEditable = data.node?.flow || data.type === "CustomComponent";
useEffect(() => {
updateNodeInternals(data.id);
}, [isMinimized]);
return ( return (
<> <>
<NodeToolbar> <NodeToolbar>
@ -141,7 +130,6 @@ export default function GenericNode({
}} }}
numberOfHandles={handles} numberOfHandles={handles}
showNode={showNode} showNode={showNode}
setIsMinimized={setIsMinimized}
></NodeToolbarComponent> ></NodeToolbarComponent>
</NodeToolbar> </NodeToolbar>
@ -295,7 +283,6 @@ export default function GenericNode({
} }
proxy={data.node?.template[templateField].proxy} proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
isMinimized={isMinimized}
/> />
) )
)} )}
@ -322,7 +309,6 @@ export default function GenericNode({
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}
isMinimized={isMinimized}
/> />
</> </>
)} )}
@ -563,7 +549,6 @@ export default function GenericNode({
} }
proxy={data.node?.template[templateField].proxy} proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
isMinimized={isMinimized}
/> />
) : ( ) : (
<></> <></>
@ -607,7 +592,6 @@ export default function GenericNode({
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}
isMinimized={isMinimized}
/> />
)} )}
</> </>

View file

@ -146,6 +146,7 @@ export default function CodeAreaModal({
<BaseModal.Content> <BaseModal.Content>
<Input <Input
value={code} value={code}
readOnly
className="absolute left-[500%] top-[500%]" className="absolute left-[500%] top-[500%]"
id="codeValue" id="codeValue"
/> />

View file

@ -32,7 +32,6 @@ export default function NodeToolbarComponent({
setShowNode, setShowNode,
numberOfHandles, numberOfHandles,
showNode, showNode,
setIsMinimized,
}: nodeToolbarPropsType): JSX.Element { }: nodeToolbarPropsType): JSX.Element {
const nodeLength = Object.keys(data.node!.template).filter( const nodeLength = Object.keys(data.node!.template).filter(
(templateField) => (templateField) =>
@ -54,15 +53,7 @@ export default function NodeToolbarComponent({
const hasApiKey = useStoreStore((state) => state.hasApiKey); const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey); const validApiKey = useStoreStore((state) => state.validApiKey);
function canMinimize() { const isMinimal = numberOfHandles <= 1;
let countHandles: number = 0;
numberOfHandles.forEach((bool) => {
if (bool) countHandles += 1;
});
if (countHandles > 1) return false;
return true;
}
const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false; const isGroup = data.node?.flow ? true : false;
const paste = useFlowStore((state) => state.paste); const paste = useFlowStore((state) => state.paste);
@ -77,7 +68,6 @@ export default function NodeToolbarComponent({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [showModalAdvanced, setShowModalAdvanced] = useState(false); const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false); const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState("");
const [showOverrideModal, setShowOverrideModal] = useState(false); const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>(); const [flowComponent, setFlowComponent] = useState<FlowType>();
@ -98,10 +88,6 @@ export default function NodeToolbarComponent({
showconfirmShare, showconfirmShare,
]); ]);
useEffect(() => {
setIsMinimized(!showNode);
}, [showNode]);
const handleSelectChange = (event) => { const handleSelectChange = (event) => {
switch (event) { switch (event) {
case "advanced": case "advanced":
@ -112,7 +98,7 @@ export default function NodeToolbarComponent({
setShowNode(data.showNode ?? true ? false : true); setShowNode(data.showNode ?? true ? false : true);
break; break;
case "Download": case "Download":
downloadNode(createFlowComponent(cloneDeep(data), version)); downloadNode(flowComponent!);
break; break;
case "SaveAll": case "SaveAll":
saveComponent(cloneDeep(data), false); saveComponent(cloneDeep(data), false);
@ -124,8 +110,7 @@ export default function NodeToolbarComponent({
break; break;
case "ungroup": case "ungroup":
takeSnapshot(); takeSnapshot();
updateFlowPosition(position, data.node?.flow!); expandGroupNode(data.id, updateFlowPosition(position, data.node?.flow!), data.node!.template, nodes, edges, setNodes, setEdges);
expandGroupNode(data, nodes, edges, setNodes, setEdges);
break; break;
case "override": case "override":
setShowOverrideModal(true); setShowOverrideModal(true);
@ -196,7 +181,7 @@ export default function NodeToolbarComponent({
</ShadTooltip> </ShadTooltip>
)} )}
<Select onValueChange={handleSelectChange} value={selectedValue}> <Select onValueChange={handleSelectChange}>
<ShadTooltip content="More" side="top"> <ShadTooltip content="More" side="top">
<SelectTrigger> <SelectTrigger>
<div> <div>

View file

@ -24,6 +24,7 @@ import useAlertStore from "./alertStore";
import { useDarkStore } from "./darkStore"; import { useDarkStore } from "./darkStore";
import useFlowStore from "./flowStore"; import useFlowStore from "./flowStore";
import { useTypesStore } from "./typesStore"; import { useTypesStore } from "./typesStore";
import { cloneDeep } from "lodash";
let saveTimeoutId: NodeJS.Timeout | null = null; let saveTimeoutId: NodeJS.Timeout | null = null;
@ -328,13 +329,16 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
takeSnapshot: () => { takeSnapshot: () => {
const currentFlowId = get().currentFlowId; const currentFlowId = get().currentFlowId;
// push the current graph to the past state // push the current graph to the past state
const newState = useFlowStore.getState(); const flowStore = useFlowStore.getState();
const newState = {nodes: cloneDeep(flowStore.nodes), edges: cloneDeep(flowStore.edges)};
const pastLength = past[currentFlowId]?.length ?? 0; const pastLength = past[currentFlowId]?.length ?? 0;
if ( if (
pastLength > 0 && pastLength > 0 &&
JSON.stringify(past[currentFlowId][pastLength - 1]) !== JSON.stringify(past[currentFlowId][pastLength - 1]) ===
JSON.stringify(newState) JSON.stringify(newState)
) { )
return;
if (pastLength > 0) {
past[currentFlowId] = past[currentFlowId].slice( past[currentFlowId] = past[currentFlowId].slice(
pastLength - defaultOptions.maxHistorySize + 1, pastLength - defaultOptions.maxHistorySize + 1,
pastLength pastLength

View file

@ -53,7 +53,6 @@ export type ParameterComponentType = {
showNode?: boolean; showNode?: boolean;
index?: string; index?: string;
onCloseModal?: (close: boolean) => void; onCloseModal?: (close: boolean) => void;
isMinimized?: boolean;
}; };
export type InputListComponentType = { export type InputListComponentType = {
value: string[]; value: string[];
@ -479,9 +478,8 @@ export type nodeToolbarPropsType = {
deleteNode: (idx: string) => void; deleteNode: (idx: string) => void;
position: XYPosition; position: XYPosition;
setShowNode: (boolean: any) => void; setShowNode: (boolean: any) => void;
numberOfHandles: boolean[] | []; numberOfHandles: number;
showNode: boolean; showNode: boolean;
setIsMinimized: (boolean: boolean) => void;
}; };
export type parsedDataType = { export type parsedDataType = {

View file

@ -1,4 +1,4 @@
import _, { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { import {
Connection, Connection,
Edge, Edge,
@ -45,7 +45,7 @@ import {
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
export function cleanEdges(nodes: Node[], edges: Edge[]) { export function cleanEdges(nodes: Node[], edges: Edge[]) {
let newEdges = _.cloneDeep(edges); let newEdges = cloneDeep(edges);
edges.forEach((edge) => { edges.forEach((edge) => {
// check if the source and target node still exists // check if the source and target node still exists
const sourceNode = nodes.find((node) => node.id === edge.source); const sourceNode = nodes.find((node) => node.id === edge.source);
@ -88,7 +88,7 @@ export function cleanEdges(nodes: Node[], edges: Edge[]) {
} }
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) { export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
let newNodes = _.cloneDeep(data); let newNodes = cloneDeep(data);
newNodes.forEach((node: Node) => { newNodes.forEach((node: Node) => {
node.selected = false; node.selected = false;
}); });
@ -129,7 +129,7 @@ export function isValidConnection(
} }
export function removeApiKeys(flow: FlowType): FlowType { export function removeApiKeys(flow: FlowType): FlowType {
let cleanFLow = _.cloneDeep(flow); let cleanFLow = cloneDeep(flow);
cleanFLow.data!.nodes.forEach((node) => { cleanFLow.data!.nodes.forEach((node) => {
for (const key in node.data.node.template) { for (const key in node.data.node.template) {
if (node.data.node.template[key].password) { if (node.data.node.template[key].password) {
@ -144,7 +144,7 @@ export function updateTemplate(
reference: APITemplateType, reference: APITemplateType,
objectToUpdate: APITemplateType objectToUpdate: APITemplateType
): APITemplateType { ): APITemplateType {
let clonedObject: APITemplateType = _.cloneDeep(reference); let clonedObject: APITemplateType = cloneDeep(reference);
// Loop through each key in the reference object // Loop through each key in the reference object
for (const key in clonedObject) { for (const key in clonedObject) {
@ -349,7 +349,7 @@ export function updateEdgesHandleIds({
edges, edges,
nodes, nodes,
}: updateEdgesHandleIdsType): Edge[] { }: updateEdgesHandleIdsType): Edge[] {
let newEdges = _.cloneDeep(edges); let newEdges = cloneDeep(edges);
newEdges.forEach((edge) => { newEdges.forEach((edge) => {
const sourceNodeId = edge.source; const sourceNodeId = edge.source;
const targetNodeId = edge.target; const targetNodeId = edge.target;
@ -653,10 +653,13 @@ export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
x: NewPosition.x - middlePoint.x, x: NewPosition.x - middlePoint.x,
y: NewPosition.y - middlePoint.y, y: NewPosition.y - middlePoint.y,
}; };
flow.data!.nodes.forEach((node) => { return {...flow, data: {...flow.data!, nodes: flow.data!.nodes.map((node) => ({
node.position.x += deltaPosition.x; ...node,
node.position.y += deltaPosition.y; position: {
}); x: node.position.x + deltaPosition.x,
y: node.position.y + deltaPosition.y,
},
}))}};
} }
export function concatFlows( export function concatFlows(
@ -759,7 +762,7 @@ export function mergeNodeTemplates({
*/ */
let template: APITemplateType = {}; let template: APITemplateType = {};
nodes.forEach((node) => { nodes.forEach((node) => {
let nodeTemplate = _.cloneDeep(node.data.node!.template); let nodeTemplate = cloneDeep(node.data.node!.template);
Object.keys(nodeTemplate) Object.keys(nodeTemplate)
.filter((field_name) => field_name.charAt(0) !== "_") .filter((field_name) => field_name.charAt(0) !== "_")
.forEach((key) => { .forEach((key) => {
@ -844,9 +847,9 @@ export function generateNodeFromFlow(
getNodeId: (type: string) => string getNodeId: (type: string) => string
): NodeType { ): NodeType {
const { nodes } = flow.data!; const { nodes } = flow.data!;
const outputNode = _.cloneDeep(findLastNode(flow.data!)); const outputNode = cloneDeep(findLastNode(flow.data!));
const position = getMiddlePoint(nodes); const position = getMiddlePoint(nodes);
let data = _.cloneDeep(flow); let data = cloneDeep(flow);
const id = getNodeId(outputNode?.data.type!); const id = getNodeId(outputNode?.data.type!);
const newGroupNode: NodeType = { const newGroupNode: NodeType = {
data: { data: {
@ -907,94 +910,6 @@ export function connectedInputNodesOnHandle(
return connectedNodes; return connectedNodes;
} }
export function ungroupNode(
groupNode: NodeDataType,
BaseFlow: ReactFlowJsonObject
) {
const { template, flow } = groupNode.node!;
const gNodes: NodeType[] = flow!.data!.nodes;
const gEdges = flow!.data!.edges;
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
BaseFlow.edges.forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) {
let type = targetHandle.type;
let field = targetHandle.proxy.field;
let proxyId = targetHandle.proxy.id;
let inputTypes = targetHandle.inputTypes;
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
if (node) {
newEdge.target = proxyId;
let newTargetHandle: targetHandleType = {
fieldName: field,
type,
id: proxyId,
inputTypes: inputTypes,
};
if (node.data.node?.flow) {
newTargetHandle.proxy = {
field: node.data.node.template[field].proxy?.field!,
id: node.data.node.template[field].proxy?.id!,
};
}
newEdge.data.targetHandle = newTargetHandle;
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
}
}
}
if (newEdge.source === groupNode.id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
if (edge.target === groupNode.id || edge.source === groupNode.id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let display_name: string | undefined;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
}
});
const nodes = [
...BaseFlow.nodes.filter((n) => n.id !== groupNode.id),
...gNodes,
];
const edges = [
...BaseFlow.edges.filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id
),
...gEdges,
...updatedEdges,
];
BaseFlow.nodes = nodes;
BaseFlow.edges = edges;
}
function updateProxyIdsOnTemplate( function updateProxyIdsOnTemplate(
template: APITemplateType, template: APITemplateType,
idsMap: { [key: string]: string } idsMap: { [key: string]: string }
@ -1031,24 +946,25 @@ export function processFlowEdges(flow: FlowType) {
} }
export function expandGroupNode( export function expandGroupNode(
groupNode: NodeDataType, id: string,
flow: FlowType,
template: APITemplateType,
nodes: Node[], nodes: Node[],
edges: Edge[], edges: Edge[],
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void, setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
) { ) {
const { template, flow } = _.cloneDeep(groupNode.node!);
const idsMap = updateIds(flow!.data!); const idsMap = updateIds(flow!.data!);
updateProxyIdsOnTemplate(template, idsMap); updateProxyIdsOnTemplate(template, idsMap);
let flowEdges = edges; let flowEdges = edges;
updateEdgesIds(flowEdges, idsMap); updateEdgesIds(flowEdges, idsMap);
const gNodes: NodeType[] = flow?.data?.nodes!; const gNodes: NodeType[] = cloneDeep(flow?.data?.nodes!);
const gEdges = flow!.data!.edges; const gEdges = cloneDeep(flow!.data!.edges);
//redirect edges to correct proxy node //redirect edges to correct proxy node
let updatedEdges: Edge[] = []; let updatedEdges: Edge[] = [];
flowEdges.forEach((edge) => { flowEdges.forEach((edge) => {
let newEdge = _.cloneDeep(edge); let newEdge = cloneDeep(edge);
if (newEdge.target === groupNode.id) { if (newEdge.target === id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle; const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) { if (targetHandle.proxy) {
let type = targetHandle.type; let type = targetHandle.type;
@ -1075,8 +991,8 @@ export function expandGroupNode(
} }
} }
} }
if (newEdge.source === groupNode.id) { if (newEdge.source === id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!)); const lastNode = cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id; newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse( let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle! newEdge.sourceHandle!
@ -1085,7 +1001,7 @@ export function expandGroupNode(
newEdge.data.sourceHandle = newSourceHandle; newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle); newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
} }
if (edge.target === groupNode.id || edge.source === groupNode.id) { if (edge.target === id || edge.source === id) {
updatedEdges.push(newEdge); updatedEdges.push(newEdge);
} }
}); });
@ -1122,12 +1038,12 @@ export function expandGroupNode(
}); });
const filteredNodes = [ const filteredNodes = [
...nodes.filter((n) => n.id !== groupNode.id), ...nodes.filter((n) => n.id !== id),
...gNodes, ...gNodes,
]; ];
const filteredEdges = [ const filteredEdges = [
...edges.filter( ...edges.filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id (e) => e.target !== id && e.source !== id
), ),
...gEdges, ...gEdges,
...updatedEdges, ...updatedEdges,
@ -1136,17 +1052,6 @@ export function expandGroupNode(
setEdges(filteredEdges); setEdges(filteredEdges);
} }
export function processFlow(FlowObject: ReactFlowJsonObject) {
let clonedFLow = _.cloneDeep(FlowObject);
clonedFLow.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) {
processFlow(node.data.node!.flow!.data!);
ungroupNode(node.data, clonedFLow);
}
});
return clonedFLow;
}
export function getGroupStatus( export function getGroupStatus(
flow: FlowType, flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } } ssData: { [key: string]: { valid: boolean; params: string } }
@ -1170,13 +1075,12 @@ export function createFlowComponent(
nodeData: NodeDataType, nodeData: NodeDataType,
version: string version: string
): FlowType { ): FlowType {
nodeData.node!.official = false;
const flowNode: FlowType = { const flowNode: FlowType = {
data: { data: {
edges: [], edges: [],
nodes: [ nodes: [
{ {
data: nodeData, data: {...nodeData, node: {...nodeData.node, official: false}},
id: nodeData.id, id: nodeData.id,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
type: "genericNode", type: "genericNode",