feat(nodes): add start node and create new node function
This commit is contained in:
3
client/public/icons/node/home.svg
Normal file
3
client/public/icons/node/home.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 16H5V10H11V16H14V7L8 2.5L2 7V16ZM0 18V6L8 0L16 6V18H9V12H7V18H0Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 198 B |
@@ -7,36 +7,38 @@ import {
|
|||||||
Node,
|
Node,
|
||||||
Edge,
|
Edge,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
||||||
import { Dropdown, DropdownProps } from "antd";
|
import { Dropdown, DropdownProps } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { edgeTitleGenerator } from "@/utils/edge";
|
import { edgeTitleGenerator } from "@/utils/edge";
|
||||||
import IfElseNode from "./nodes/IfElseNode";
|
import IfElseNode from "./nodes/IfElseNode";
|
||||||
import AppropriationNode from "./nodes/AppropriationNode";
|
import AppropriationNode from "./nodes/AppropriationNode";
|
||||||
|
import StartNode from "./nodes/StartNode";
|
||||||
const nodeTypes = {
|
|
||||||
ifElse: IfElseNode,
|
|
||||||
appropriation: AppropriationNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
type: "ifElse",
|
type: "startNode",
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 0 },
|
||||||
data: { condition: "B=2" },
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
type: "appropriation",
|
type: "ifElse",
|
||||||
position: { x: 100, y: 300 },
|
position: { x: 100, y: 200 },
|
||||||
data: { value: "Выбрать {{account.email}}" },
|
data: { condition: "B=2" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
type: "appropriation",
|
type: "appropriation",
|
||||||
position: { x: 400, y: 300 },
|
position: { x: 100, y: 400 },
|
||||||
|
data: { value: "Выбрать {{account.email}}" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
type: "appropriation",
|
||||||
|
position: { x: 400, y: 400 },
|
||||||
data: { value: "Выбрать {{account.role}}" },
|
data: { value: "Выбрать {{account.role}}" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -92,6 +94,9 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
const handleId = handleElement.getAttribute("data-handleid");
|
const handleId = handleElement.getAttribute("data-handleid");
|
||||||
if (!handleId) return;
|
if (!handleId) return;
|
||||||
|
|
||||||
|
const handlePos = handleElement.getAttribute("data-handlepos");
|
||||||
|
if (handlePos === "top") return;
|
||||||
|
|
||||||
setSelectedHandleId(`${node.id}-${handleId}`);
|
setSelectedHandleId(`${node.id}-${handleId}`);
|
||||||
|
|
||||||
const flowWrapper = document.querySelector(".react-flow") as HTMLElement;
|
const flowWrapper = document.querySelector(".react-flow") as HTMLElement;
|
||||||
@@ -100,7 +105,7 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
const handleRect = handleElement.getBoundingClientRect();
|
const handleRect = handleElement.getBoundingClientRect();
|
||||||
setMenuPosition({
|
setMenuPosition({
|
||||||
x: handleRect.right - wrapperRect.left,
|
x: handleRect.right - wrapperRect.left,
|
||||||
y: handleRect.top - wrapperRect.top,
|
y: handleRect.top - wrapperRect.top + 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
const label = edgeTitleGenerator(edges.length + 1);
|
const label = edgeTitleGenerator(edges.length + 1);
|
||||||
|
|
||||||
const newEdge: Edge = {
|
const newEdge: Edge = {
|
||||||
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}`,
|
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}:${label}`,
|
||||||
source: sourceNodeId,
|
source: sourceNodeId,
|
||||||
sourceHandle: sourceHandleId,
|
sourceHandle: sourceHandleId,
|
||||||
target: targetNodeId,
|
target: targetNodeId,
|
||||||
@@ -128,7 +133,61 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
setSelectedHandleId(null);
|
setSelectedHandleId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems = nodes
|
const handleCreateNode = (type: string) => {
|
||||||
|
if (!selectedHandleId) return;
|
||||||
|
const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-");
|
||||||
|
const sourceNode = nodes.find((n) => n.id === sourceNodeId);
|
||||||
|
const newId = (
|
||||||
|
Math.max(...nodes.map((n) => Number(n.id) || 0)) + 1
|
||||||
|
).toString();
|
||||||
|
let newNode;
|
||||||
|
if (type === "ifElse") {
|
||||||
|
newNode = {
|
||||||
|
id: newId,
|
||||||
|
type: "ifElse",
|
||||||
|
position: {
|
||||||
|
x: (sourceNode?.position.x || 0) + 200,
|
||||||
|
y: (sourceNode?.position.y || 0) + 100,
|
||||||
|
},
|
||||||
|
data: { condition: "" },
|
||||||
|
};
|
||||||
|
} else if (type === "appropriation") {
|
||||||
|
newNode = {
|
||||||
|
id: newId,
|
||||||
|
type: "appropriation",
|
||||||
|
position: {
|
||||||
|
x: (sourceNode?.position.x || 0) + 200,
|
||||||
|
y: (sourceNode?.position.y || 0) + 100,
|
||||||
|
},
|
||||||
|
data: { value: "" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newNode) {
|
||||||
|
setNodes((nds) => [...nds, newNode]);
|
||||||
|
const label = edgeTitleGenerator(edges.length + 1);
|
||||||
|
const newEdge = {
|
||||||
|
id: `e${sourceNodeId}-${sourceHandleId}-${newId}:${label}`,
|
||||||
|
source: sourceNodeId,
|
||||||
|
sourceHandle: sourceHandleId,
|
||||||
|
target: newId,
|
||||||
|
label,
|
||||||
|
...customEdgeStyle,
|
||||||
|
};
|
||||||
|
setEdges((eds) => [...eds, newEdge]);
|
||||||
|
setMenuVisible(false);
|
||||||
|
setSelectedHandleId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newNodeTypes = [
|
||||||
|
{ key: "ifElse", label: t("ifElseNode") },
|
||||||
|
{
|
||||||
|
key: "appropriation",
|
||||||
|
label: t("appropriationNode"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const existingNodes = nodes
|
||||||
.filter((node) => node.id !== selectedHandleId?.split("-")[0])
|
.filter((node) => node.id !== selectedHandleId?.split("-")[0])
|
||||||
.filter((node) => {
|
.filter((node) => {
|
||||||
if (!selectedHandleId) return false;
|
if (!selectedHandleId) return false;
|
||||||
@@ -139,12 +198,35 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
edge.sourceHandle === sourceHandleId &&
|
edge.sourceHandle === sourceHandleId &&
|
||||||
edge.target === node.id
|
edge.target === node.id
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
.map((node) => ({
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: "connectToExisting",
|
||||||
|
label: t("connectToExisting"),
|
||||||
|
children: existingNodes.map((node) => ({
|
||||||
key: node.id,
|
key: node.id,
|
||||||
label: t("connectTo", { nodeId: node.id }),
|
label: t("connectTo", { nodeId: node.id }),
|
||||||
onClick: () => handleMenuItemClick(node.id),
|
onClick: () => handleMenuItemClick(node.id),
|
||||||
}));
|
})),
|
||||||
|
},
|
||||||
|
...newNodeTypes.map((nt) => ({
|
||||||
|
key: `createNew-${nt.key}`,
|
||||||
|
label: nt.label,
|
||||||
|
onClick: () => handleCreateNode(nt.key),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeTypes = useMemo(
|
||||||
|
() => ({
|
||||||
|
startNode: (props: any) => <StartNode {...props} edges={edges} />,
|
||||||
|
ifElse: (props: any) => <IfElseNode {...props} edges={edges} />,
|
||||||
|
appropriation: (props: any) => (
|
||||||
|
<AppropriationNode {...props} edges={edges} />
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[edges]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
@@ -165,6 +247,8 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
}}
|
}}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
|
minZoom={0.5}
|
||||||
|
maxZoom={1.0}
|
||||||
>
|
>
|
||||||
<Background color="#F2F2F2" />
|
<Background color="#F2F2F2" />
|
||||||
<Controls
|
<Controls
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
|
import { Handle, Node, NodeProps, Position, Edge } from "@xyflow/react";
|
||||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
import { HANDLE_STYLE_CONNECTED } from "./defaultHandleStyle";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type AppropriationNodeData = { value: string };
|
type AppropriationNodeData = { value: string; edges?: Edge[] };
|
||||||
|
|
||||||
export default function AppropriationNode({
|
export default function AppropriationNode({
|
||||||
data,
|
data,
|
||||||
}: NodeProps<Node & AppropriationNodeData>) {
|
id,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<Node & AppropriationNodeData> & { edges?: Edge[] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -48,7 +51,9 @@ export default function AppropriationNode({
|
|||||||
{data.value as string}
|
{data.value as string}
|
||||||
</div>
|
</div>
|
||||||
<Handle
|
<Handle
|
||||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
style={{
|
||||||
|
...HANDLE_STYLE_CONNECTED,
|
||||||
|
}}
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
id="input"
|
id="input"
|
||||||
|
@@ -1,13 +1,31 @@
|
|||||||
import { Handle, NodeProps, Position, Node } from "@xyflow/react";
|
import { Handle, NodeProps, Position, Node, Edge } from "@xyflow/react";
|
||||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
import {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_CONNECTED_V,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED_V,
|
||||||
|
} from "./defaultHandleStyle";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface IfElseNodeProps extends Node {
|
interface IfElseNodeProps extends Node {
|
||||||
condition: string;
|
condition: string;
|
||||||
|
edges?: Edge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
export default function IfElseNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<IfElseNodeProps> & { edges?: Edge[] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isHandle1Connected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "1"
|
||||||
|
);
|
||||||
|
const isHandle2Connected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "2"
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -16,7 +34,8 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
width: "248px",
|
width: "248px",
|
||||||
height: "144px",
|
minHeight: "144px",
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -43,12 +62,26 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingLeft: "12px",
|
padding: "12px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
height: "48px",
|
minHeight: "48px",
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("conditionIf", { condition: data.condition })}
|
{t("conditionIf", { condition: data.condition })}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="1"
|
||||||
|
style={{
|
||||||
|
...(isHandle1Connected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED_V }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED_V }),
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
|
||||||
@@ -64,18 +97,21 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
|||||||
{t("conditionElse")}
|
{t("conditionElse")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Top} id="input" />
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="target"
|
||||||
position={Position.Right}
|
position={Position.Top}
|
||||||
id="1"
|
id="input"
|
||||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
style={{ ...HANDLE_STYLE_CONNECTED }}
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="2"
|
id="2"
|
||||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
style={
|
||||||
|
isHandle2Connected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED }
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
83
client/src/components/nodes/StartNode.tsx
Normal file
83
client/src/components/nodes/StartNode.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Edge, Handle, Node, NodeProps, Position } from "@xyflow/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
} from "./defaultHandleStyle";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface StartNodeProps extends Node {
|
||||||
|
edges?: Edge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StartNode({
|
||||||
|
id,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<StartNodeProps> & { edges?: Edge[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isHandleConnected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "1"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "0px solid",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "248px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "12px",
|
||||||
|
height: "40px",
|
||||||
|
fontWeight: "600px",
|
||||||
|
fontSize: "16px",
|
||||||
|
gap: "12px",
|
||||||
|
backgroundColor: "#D4E0BD",
|
||||||
|
borderRadius: "8px 8px 0 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ height: "16px", width: "18px" }}
|
||||||
|
src="/icons/node/home.svg"
|
||||||
|
alt="start logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{t("startNode")}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("startNodeDescription")}
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="1"
|
||||||
|
style={
|
||||||
|
isHandleConnected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,34 @@
|
|||||||
const DEFAULT_HANDLE_STYLE = {
|
// horizontal
|
||||||
|
const HANDLE_STYLE_CONNECTED = {
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor: "#BDBDBD",
|
backgroundColor: "#BDBDBD",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DEFAULT_HANDLE_STYLE;
|
// horizontal
|
||||||
|
const HANDLE_STYLE_DISCONNECTED = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
backgroundColor: "#C7D95A",
|
||||||
|
borderColor: "#606060",
|
||||||
|
borderWidth: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
const HANDLE_STYLE_CONNECTED_V = {
|
||||||
|
...HANDLE_STYLE_CONNECTED,
|
||||||
|
right: "-4px",
|
||||||
|
};
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
const HANDLE_STYLE_DISCONNECTED_V = {
|
||||||
|
...HANDLE_STYLE_DISCONNECTED,
|
||||||
|
right: "-10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
HANDLE_STYLE_CONNECTED_V,
|
||||||
|
HANDLE_STYLE_DISCONNECTED_V,
|
||||||
|
};
|
||||||
|
@@ -52,6 +52,9 @@ i18n
|
|||||||
editAccountMessage: "User successfully updated!",
|
editAccountMessage: "User successfully updated!",
|
||||||
you: "(You)",
|
you: "(You)",
|
||||||
// nodes
|
// nodes
|
||||||
|
startNode: "Start",
|
||||||
|
startNodeDescription: "Start",
|
||||||
|
connectToExisting: "Connect to existing",
|
||||||
editNode: "Editing a block",
|
editNode: "Editing a block",
|
||||||
connectTo: "Connect to {{nodeId}}",
|
connectTo: "Connect to {{nodeId}}",
|
||||||
ifElseNode: "IF - ELSE",
|
ifElseNode: "IF - ELSE",
|
||||||
@@ -102,6 +105,9 @@ i18n
|
|||||||
editAccountMessage: "Пользователь успешно обновлен!",
|
editAccountMessage: "Пользователь успешно обновлен!",
|
||||||
you: "(Вы)",
|
you: "(Вы)",
|
||||||
// nodes
|
// nodes
|
||||||
|
startNode: "Запуск",
|
||||||
|
startNodeDescription: "Включение",
|
||||||
|
connectToExisting: "Подключить к существующей",
|
||||||
editNode: "Редактирование блока",
|
editNode: "Редактирование блока",
|
||||||
connectTo: `Подключить к {{nodeId}}`,
|
connectTo: `Подключить к {{nodeId}}`,
|
||||||
ifElseNode: "ЕСЛИ - ТО",
|
ifElseNode: "ЕСЛИ - ТО",
|
||||||
|
Reference in New Issue
Block a user