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,
|
||||
Edge,
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
||||
import { Dropdown, DropdownProps } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { edgeTitleGenerator } from "@/utils/edge";
|
||||
import IfElseNode from "./nodes/IfElseNode";
|
||||
import AppropriationNode from "./nodes/AppropriationNode";
|
||||
|
||||
const nodeTypes = {
|
||||
ifElse: IfElseNode,
|
||||
appropriation: AppropriationNode,
|
||||
};
|
||||
import StartNode from "./nodes/StartNode";
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: "1",
|
||||
type: "ifElse",
|
||||
position: { x: 100, y: 100 },
|
||||
data: { condition: "B=2" },
|
||||
type: "startNode",
|
||||
position: { x: 100, y: 0 },
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "appropriation",
|
||||
position: { x: 100, y: 300 },
|
||||
data: { value: "Выбрать {{account.email}}" },
|
||||
type: "ifElse",
|
||||
position: { x: 100, y: 200 },
|
||||
data: { condition: "B=2" },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
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}}" },
|
||||
},
|
||||
];
|
||||
@@ -92,6 +94,9 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||
const handleId = handleElement.getAttribute("data-handleid");
|
||||
if (!handleId) return;
|
||||
|
||||
const handlePos = handleElement.getAttribute("data-handlepos");
|
||||
if (handlePos === "top") return;
|
||||
|
||||
setSelectedHandleId(`${node.id}-${handleId}`);
|
||||
|
||||
const flowWrapper = document.querySelector(".react-flow") as HTMLElement;
|
||||
@@ -100,7 +105,7 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||
const handleRect = handleElement.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
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 newEdge: Edge = {
|
||||
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}`,
|
||||
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}:${label}`,
|
||||
source: sourceNodeId,
|
||||
sourceHandle: sourceHandleId,
|
||||
target: targetNodeId,
|
||||
@@ -128,7 +133,61 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||
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) => {
|
||||
if (!selectedHandleId) return false;
|
||||
@@ -139,12 +198,35 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||
edge.sourceHandle === sourceHandleId &&
|
||||
edge.target === node.id
|
||||
);
|
||||
})
|
||||
.map((node) => ({
|
||||
key: node.id,
|
||||
label: t("connectTo", { nodeId: node.id }),
|
||||
onClick: () => handleMenuItemClick(node.id),
|
||||
}));
|
||||
});
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: "connectToExisting",
|
||||
label: t("connectToExisting"),
|
||||
children: existingNodes.map((node) => ({
|
||||
key: node.id,
|
||||
label: t("connectTo", { nodeId: 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 (
|
||||
<ReactFlow
|
||||
@@ -165,6 +247,8 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
minZoom={0.5}
|
||||
maxZoom={1.0}
|
||||
>
|
||||
<Background color="#F2F2F2" />
|
||||
<Controls
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
|
||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
||||
import { Handle, Node, NodeProps, Position, Edge } from "@xyflow/react";
|
||||
import { HANDLE_STYLE_CONNECTED } from "./defaultHandleStyle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type AppropriationNodeData = { value: string };
|
||||
type AppropriationNodeData = { value: string; edges?: Edge[] };
|
||||
|
||||
export default function AppropriationNode({
|
||||
data,
|
||||
}: NodeProps<Node & AppropriationNodeData>) {
|
||||
id,
|
||||
edges = [],
|
||||
}: NodeProps<Node & AppropriationNodeData> & { edges?: Edge[] }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -48,7 +51,9 @@ export default function AppropriationNode({
|
||||
{data.value as string}
|
||||
</div>
|
||||
<Handle
|
||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||
style={{
|
||||
...HANDLE_STYLE_CONNECTED,
|
||||
}}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
|
@@ -1,13 +1,31 @@
|
||||
import { Handle, NodeProps, Position, Node } from "@xyflow/react";
|
||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
||||
import { Handle, NodeProps, Position, Node, Edge } from "@xyflow/react";
|
||||
import {
|
||||
HANDLE_STYLE_CONNECTED,
|
||||
HANDLE_STYLE_CONNECTED_V,
|
||||
HANDLE_STYLE_DISCONNECTED,
|
||||
HANDLE_STYLE_DISCONNECTED_V,
|
||||
} from "./defaultHandleStyle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
interface IfElseNodeProps extends Node {
|
||||
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 isHandle1Connected = edges.some(
|
||||
(e: Edge) => e.source === id && e.sourceHandle === "1"
|
||||
);
|
||||
const isHandle2Connected = edges.some(
|
||||
(e: Edge) => e.source === id && e.sourceHandle === "2"
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -16,7 +34,8 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
||||
borderRadius: 8,
|
||||
backgroundColor: "white",
|
||||
width: "248px",
|
||||
height: "144px",
|
||||
minHeight: "144px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -43,12 +62,26 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: "12px",
|
||||
padding: "12px",
|
||||
fontSize: "14px",
|
||||
height: "48px",
|
||||
minHeight: "48px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{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 style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||
|
||||
@@ -64,18 +97,21 @@ export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
||||
{t("conditionElse")}
|
||||
</div>
|
||||
|
||||
<Handle type="target" position={Position.Top} id="input" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="1"
|
||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
style={{ ...HANDLE_STYLE_CONNECTED }}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="2"
|
||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||
style={
|
||||
isHandle2Connected
|
||||
? { ...HANDLE_STYLE_CONNECTED }
|
||||
: { ...HANDLE_STYLE_DISCONNECTED }
|
||||
}
|
||||
/>
|
||||
</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,
|
||||
height: 8,
|
||||
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!",
|
||||
you: "(You)",
|
||||
// nodes
|
||||
startNode: "Start",
|
||||
startNodeDescription: "Start",
|
||||
connectToExisting: "Connect to existing",
|
||||
editNode: "Editing a block",
|
||||
connectTo: "Connect to {{nodeId}}",
|
||||
ifElseNode: "IF - ELSE",
|
||||
@@ -102,6 +105,9 @@ i18n
|
||||
editAccountMessage: "Пользователь успешно обновлен!",
|
||||
you: "(Вы)",
|
||||
// nodes
|
||||
startNode: "Запуск",
|
||||
startNodeDescription: "Включение",
|
||||
connectToExisting: "Подключить к существующей",
|
||||
editNode: "Редактирование блока",
|
||||
connectTo: `Подключить к {{nodeId}}`,
|
||||
ifElseNode: "ЕСЛИ - ТО",
|
||||
|
Reference in New Issue
Block a user