import React, { useRef, useState, useCallback, useEffect } from 'react';
import ReactFlow, {
applyNodeChanges,
applyEdgeChanges,
addEdge,
} from 'reactflow';
import { nodeTypes, edgeTypes } from './element.types.js';
import { getNodeSublabel, validateConnection } from './node.utils.jsx';
import ControlPanel from './control.panel.jsx';
import SidebarPanel from './sidebar.panel.jsx';
import VariablesDialog from './variables.dialog.jsx';
import { nanoid } from 'nanoid';
import { Background, Controls, MiniMap, Panel, MarkerType, reconnectEdge } from 'reactflow';
const initialNodes = window.WAIC_WORKFLOW.flow?.nodes || [];
//const initialNodes = [{ id: '1', type: 'trigger', position: { x: 350, y: 200 }, data: {} }];
const initialEdges = window.WAIC_WORKFLOW.flow?.edges || [];
const savedViewport = window.WAIC_WORKFLOW.flow?.viewport || false;
let nodeCounter = Math.max(0,...initialNodes.map(n => parseInt(n.id.replace(/\D/g, '')) || 0)) + 1;
let edgeCounter = Math.max(0,...initialEdges.map(n => parseInt(n.id.replace(/\D/g, '')) || 0)) + 1;
window.WAIC_WORKFLOW.editor = wp.editor || {};
window.WAIC_WORKFLOW.isDirty = false;
if (typeof _.isArray !== 'function') {
_.isArray = Array.isArray;
}
export default function BuilderWrapper() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const [selectedNode, setSelectedNode] = useState(null);
const waicWorkflow = window.WAIC_WORKFLOW || {};
const taskId = parseInt(waicWorkflow.task_id) || 0;
const [showVariables, setShowVariables] = useState(false);
const [variables, setVariables] = useState([]);
const [activeField, setActiveField] = useState(null);
function collectVariablesForNode(nodeId, nodes, edges) {
const visited = new Set();
const groups = [];
const blocks = window.WAIC_WORKFLOW?.blocks || {};
function traverse(currentId, incomingHandle = null, skipSelf = false) {
if (visited.has(currentId)) return;
visited.add(currentId);
const node = nodes.find(n => n.id === currentId);
if (node && !skipSelf) {
const nodeType = node.type;
const nodeCode = node.data?.code;
const nodeCat = node.data?.category;
const list = blocks[nodeType + 's']?.[nodeCat]?.list || [];
const block = list.find((item) => item.code === nodeCode);
const variables = block?.variables || [];
const filteredVars = {};
Object.entries(variables).forEach(([key, val]) => {
if (key === 'loop_vars') {
if (incomingHandle === 'output-then') {
Object.entries(val).forEach(([loopKey, loopLabel]) => {
filteredVars[loopKey] = loopLabel;
});
}
} else {
filteredVars[key] = val;
}
});
if (Object.keys(filteredVars).length > 0) {
groups.push({
nodeId: node.id,
label: node.data?.label,
sublabel: getNodeSublabel(node.data, nodeType + 's'),
variables: filteredVars,
});
}
}
const parentEdges = edges.filter(e => e.target === currentId);
parentEdges.forEach(e => traverse(e.source, e.sourceHandle));
}
traverse(nodeId, null, true);
return groups;
}
const cursorRef = useRef(null);
const handleCursorCapture = (e, fieldKey) => {
cursorRef.current = {
element: e.target,
start: e.target.selectionStart,
end: e.target.selectionEnd,
fieldKey: fieldKey,
};
};
const handleOpenVariables = (fieldKey) => {
setActiveField(fieldKey);
if (selectedNode) {
const vars = collectVariablesForNode(selectedNode.id, nodes, edges);
setVariables(vars);
}
setShowVariables(true);
};
const [settingChanger, setSettingChanger] = useState(null);
const handleInsertVariable = (id, val, customKey) => {
if (!settingChanger || !activeField) return;
const metaKey = customKey ? '['+customKey+']' : '';
const variableText = `{{node#${id}.${val}${metaKey}}}`;
if (cursorRef.current?.fieldKey === activeField) {
const el = cursorRef.current.element;
const start = cursorRef.current.start;
const end = cursorRef.current.end;
const value = el.value;
const newValue = value.slice(0, start) + variableText + value.slice(end);
settingChanger(activeField, newValue);
setTimeout(() => {
el.focus();
const newPos = start + variableText.length;
el.setSelectionRange(newPos, newPos);
}, 0);
} else {
const currentValue = selectedNode.data?.settings?.[activeField] || '';
settingChanger(activeField, currentValue + variableText);
}
};
const handleAddNode = useCallback((typeNode) => {
const id = `${nodeCounter++}`;
const nodesLen = nodes.length || 0;
let x = 350;
let y = 200;
if (selectedNode) {
x = selectedNode.position.x + 180 + nodesLen * 5;
y = selectedNode.position.y;
} else if (nodesLen > 0) {
const lastNode = nodes[nodes.length-1];
x = lastNode.position.x + 180;
y = lastNode.position.y;
}
const newNode = {
id,
data: {},
type: typeNode,
position: { x, y },
};
setNodes((nds) => [...nds, newNode]);
}, [nodes, selectedNode]);
const handleDeleteNode = useCallback((id) => {
setNodes((nds) => nds.filter((node) => node.id !== id));
setEdges((eds) => eds.filter((edge) => edge.source !== id && edge.target !== id));
setSelectedNode((sel) => (sel?.id === id ? null : sel));
}, []);
const onNodesChange = useCallback(
(changes) => {
const meaningful = changes.some(c => !['dimensions','select','position'].includes(c.type));
if (meaningful) waicWorkflow.isDirty = true;
setNodes((ns) => applyNodeChanges(changes, ns));
},
[]
);
const onEdgesChange = useCallback(
(changes) => {
waicWorkflow.isDirty = true;
setEdges((es) => applyEdgeChanges(changes, es));
},
[]
);
const onConnect = useCallback((params) => {
const isValid = validateConnection({
source: params.source,
target: params.target,
sourceHandle: params.sourceHandle,
edges,
nodes,
});
if (!isValid) return;
const newEdge = {
id: `${edgeCounter++}`,
source: params.source,
target: params.target,
sourceHandle: params.sourceHandle,
targetHandle: params.targetHandle,
type: 'default',
};
setEdges((es) => [...es, newEdge]);
waicWorkflow.isDirty = true;
}, [edges]);
const onReconnect = useCallback((oldEdge, newConnection) => {
const edgesWithoutOld = edges.filter(e => e.id !== oldEdge.id);
const isValid = validateConnection({
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
edges: edgesWithoutOld,
nodes,
});
if (!isValid) return;
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
waicWorkflow.isDirty = true;
}, [edges, nodes]);
const onNodeClick = useCallback((event, node) => {
setSelectedNode(node);
}, []);
const handleNodeDragStop = (event, node) => {
updateNode({
...node,
position: node.position,
positionAbsolute: node.positionAbsolute,
data: {
...node.data,
dragged: true,
},
});
};
const updateNode = (updatedNode) => {
if (!updatedNode) {
setSelectedNode(null);
return;
}
setNodes((prevNodes) => {
const oldNode = prevNodes.find((n) => n.id === updatedNode.id);
const oldCode = oldNode?.data?.code;
const newCode = updatedNode.data?.code;
const nodeType = updatedNode.type;
const shouldCleanEdges = (nodeType === 'logic' && oldCode !== newCode);
if (shouldCleanEdges) {
setEdges((prevEdges) =>
prevEdges.filter((edge) => {
if (edge.source !== updatedNode.id) return true;
if (newCode === 'un_delay') {
return !['output-then', 'output-else'].includes(edge.sourceHandle);
} else {
return !['output-right'].includes(edge.sourceHandle);
}
})
);
}
const oldData = JSON.stringify(oldNode.data);
const newData = JSON.stringify(updatedNode.data);
if (oldData !== newData) window.WAIC_WORKFLOW.isDirty = true;
return prevNodes.map((n) => (n.id === updatedNode.id ? updatedNode : n));
});
setSelectedNode(updatedNode);
};
const [selectedEdges, setSelectedEdges] = useState([]);
const onSelectionChange = useCallback(({ nodes = [], edges = [] }) => {
setSelectedEdges(edges);
if (nodes.length === 1) {
setSelectedNode(nodes[0]); // открываем сайдбар
} else {
setSelectedNode(null); // скрываем сайдбар, если ничего не выбрано
}
}, []);
const serializeFlow = () => {
const { x, y, zoom } = reactFlowInstanceRef.current.getViewport();
return {
nodes: nodes.map(({ id, type, position, data }) => ({
id, type, position, data,
})),
edges: edges.map(({ id, source, target, sourceHandle, targetHandle, type }) => ({
id, source, target, sourceHandle, targetHandle, type,
})),
viewport: { x, y, zoom },
settings: waicWorkflow.flow?.settings || '',
version: '1.0.0',
};
};
const handleSaveFlow = () => {
jQuery.sendFormWaic({
imgBtn: jQuery('.waic-control-save'),
data: {
flow: JSON.stringify(serializeFlow()),
task_id: taskId,
title: waicWorkflow.title || '',
mod: 'workflow',
action: 'saveWorkflow',
pl: 'waic',
reqType: 'ajax',
waicNonce: window.WAIC_DATA.waicNonce,
//params: jsonInputsWaic($form, true),
},
onSuccess: function(res) {
if (!res.error && res.data && res.data.taskUrl) {
waicWorkflow.isDirty = false;
if (taskId == 0) jQuery(location).attr('href', res.data.taskUrl);
}
if (res.error && res.data && res.data.err_nodes) {
//WAIC_WORKFLOW.errors=[res.data.err_nodes];
res.data.err_nodes.map(n =>
setNodes(prev =>
prev.map(node =>
node.id === n[0]
? { ...node, data: { ...node.data, error: n[1] } }
: node
)
)
);
/*if (selectedNode) {
setSelectedNode(null);
}*/
}
}
});
};
useEffect(() => {
const handleBeforeUnload = (e) => {
if (waicWorkflow.isDirty) {
e.preventDefault();
e.returnValue = window.WAIC_WORKFLOW?.lang?.unload_dialog || 'Are you sure you want to leave without saving?';
return e.returnValue;
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
useEffect(() => {
const handleKeyDown = (e) => {
const isDeleteKey = e.key === 'Delete' || e.key === 'Backspace';
if (!isDeleteKey) return;
const el = document.activeElement;
const tag = el?.tagName?.toUpperCase();
const isFormElement =
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
tag === 'SELECT' ||
tag === 'BUTTON' ||
el?.isContentEditable;
if (isFormElement) return;
if (selectedEdges.length > 0) {
setEdges((eds) =>
eds.filter((edge) => !selectedEdges.some((sel) => sel.id === edge.id))
);
}
if (selectedNode) {
setNodes((nds) => nds.filter((node) => node.id !== selectedNode.id));
setEdges((eds) =>
eds.filter(
(edge) =>
edge.source !== selectedNode.id && edge.target !== selectedNode.id
)
);
setSelectedNode(null);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedEdges, selectedNode]);
const reactFlowInstanceRef = useRef(null);
return (
<>