diff --git a/client/src/components/ReactFlowDrawer.tsx b/client/src/components/ReactFlowDrawer.tsx index 7a32bc5..85a862e 100644 --- a/client/src/components/ReactFlowDrawer.tsx +++ b/client/src/components/ReactFlowDrawer.tsx @@ -4,46 +4,23 @@ import { Controls, useNodesState, useEdgesState, - Handle, - Position, + Node, + Edge, } from "@xyflow/react"; -import IfElseNode from "./nodes/IfElseNode"; -import { useCallback, useEffect } from "react"; +import { useState } from "react"; import customEdgeStyle from "./edges/defaultEdgeStyle"; -import { message } from "antd"; -import AppropriationNode from "./nodes/AppropriationNode"; +import { Dropdown, DropdownProps } from "antd"; import { useTranslation } from "react-i18next"; - -function CustomNode({ data }: { data: any }) { - return ( -
-
-
{data.label}
-
-
- -
-
- -
-
- ); -} +import { edgeTitleGenerator } from "@/utils/edge"; +import IfElseNode from "./nodes/IfElseNode"; +import AppropriationNode from "./nodes/AppropriationNode"; const nodeTypes = { - custom: CustomNode, ifElse: IfElseNode, appropriation: AppropriationNode, }; -const initialNodes = [ +const initialNodes: Node[] = [ { id: "1", type: "ifElse", @@ -52,9 +29,9 @@ const initialNodes = [ }, { id: "2", - type: "default", - position: { x: 400, y: 100 }, - data: { label: "Приём" }, + type: "appropriation", + position: { x: 100, y: 300 }, + data: { value: "Выбрать {{account.email}}" }, }, { id: "3", @@ -64,14 +41,24 @@ const initialNodes = [ }, ]; -const initialEdges = [ - { - id: "e1-2", - source: "1", - target: "2", - label: "A1", - ...customEdgeStyle, - }, +const initialEdges: Edge[] = [ + // { + // id: "e1-3", + // source: "1", + // sourceHandle: "1", + // target: "3", + // targetHandle: null, + // label: "A1", + // ...customEdgeStyle, + // }, + // { + // id: "e1-2", + // source: "1", + // sourceHandle: "2", + // target: "2", + // label: "B1", + // ...customEdgeStyle, + // }, ]; interface ReactFlowDrawerProps { @@ -84,56 +71,117 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const onConnect = useCallback( - (con: { source: any; target: any }) => { - const exists = edges.some( - (edge) => edge.source === con.source && edge.target === con.target - ); + const [menuVisible, setMenuVisible] = useState(false); + const [selectedHandleId, setSelectedHandleId] = useState(null); + const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); - if (exists) { - message.warning("Edge already exists"); - return; - } + const handleOpenChange: DropdownProps["onOpenChange"] = (nextOpen, info) => { + if (info.source === "trigger" || nextOpen) { + setMenuVisible(nextOpen); + } + }; - const newEdge = { - ...con, - ...customEdgeStyle, - id: `e${con.source}-${con.target}`, - label: "A1", - }; + const handleClick = (e: React.MouseEvent, node: Node) => { + e.stopPropagation(); - setEdges((eds) => [...eds, newEdge]); - }, - [edges, setEdges] - ); + const target = e.target as HTMLElement; + const handleElement = target.closest(".react-flow__handle") as HTMLElement; - useEffect(() => { - console.log("Nodes changed:", nodes); - }, [nodes]); + if (!handleElement) return; - useEffect(() => { - console.log("Edges changed:", edges); - }, [edges]); + const handleId = handleElement.getAttribute("data-handleid"); + if (!handleId) return; + + setSelectedHandleId(`${node.id}-${handleId}`); + + const flowWrapper = document.querySelector(".react-flow") as HTMLElement; + if (flowWrapper) { + const wrapperRect = flowWrapper.getBoundingClientRect(); + const handleRect = handleElement.getBoundingClientRect(); + setMenuPosition({ + x: handleRect.right - wrapperRect.left, + y: handleRect.top - wrapperRect.top, + }); + } + + setMenuVisible(true); + }; + + const handleMenuItemClick = (targetNodeId: string) => { + if (!selectedHandleId) return; + + const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-"); + + const label = edgeTitleGenerator(edges.length + 1); + + const newEdge: Edge = { + id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}`, + source: sourceNodeId, + sourceHandle: sourceHandleId, + target: targetNodeId, + label, + ...customEdgeStyle, + }; + + setEdges((eds) => [...eds, newEdge]); + setMenuVisible(false); + setSelectedHandleId(null); + }; + + const menuItems = nodes + .filter((node) => node.id !== selectedHandleId?.split("-")[0]) // исключаем текущий узел + .map((node) => ({ + key: node.id, + label: t("connectTo", { nodeId: node.id }), + onClick: () => handleMenuItemClick(node.id), + })); return ( { - console.log(e, "node clicked"); - showDrawer(); + onNodeClick={(event, node) => { + const target = event.target as HTMLElement; + + if (!target.closest(".react-flow__handle")) { + console.log("node clicked"); + showDrawer(); + } else { + handleClick(event, node); + } }} nodeTypes={nodeTypes} fitView > - - + + + + {menuVisible && ( +
+ +
+ +
+ )} ); } diff --git a/client/src/components/drawers/DrawerTitles.tsx b/client/src/components/drawers/DrawerTitles.tsx new file mode 100644 index 0000000..21b8ba6 --- /dev/null +++ b/client/src/components/drawers/DrawerTitles.tsx @@ -0,0 +1,213 @@ +import { User } from "@/types/user"; +import { Avatar, Typography } from "antd"; +import { TFunction } from "i18next"; + +interface DrawerTitleProps { + closeDrawer: () => void; + t: TFunction; +} + +interface UserEditDrawerTitleProps extends DrawerTitleProps { + login?: string; + name?: string; + email?: string | null; + user: User | null; +} + +interface UserCreateDrawerTitleProps extends DrawerTitleProps {} + +interface NodeEditDrawerTitleProps extends DrawerTitleProps {} + +const UserEditDrawerTitle = ({ + closeDrawer, + login, + name, + email, + user, + t, +}: UserEditDrawerTitleProps) => { + return ( +
+
+ close_drawer +
+ +
+ +
+ + {name} {login === user?.login ? t("you") : ""} + + + {email} + +
+
+ +
+ delete +
+
+ ); +}; + +const UserCreateDrawerTitle = ({ + closeDrawer, + t, +}: UserCreateDrawerTitleProps) => { + return ( +
+
+ close_drawer +
+ +
+ {t("newAccount")} +
+ +
+ delete +
+
+ ); +}; + +const NodeEditDrawerTitle = ({ closeDrawer, t }: NodeEditDrawerTitleProps) => { + return ( +
+
+ close_drawer +
+ +
+ {t("editNode")} +
+ +
+ delete +
+
+ ); +}; + +export { UserEditDrawerTitle, UserCreateDrawerTitle, NodeEditDrawerTitle }; diff --git a/client/src/components/nodes/AppropriationNode.tsx b/client/src/components/nodes/AppropriationNode.tsx index b9888ee..c0b1785 100644 --- a/client/src/components/nodes/AppropriationNode.tsx +++ b/client/src/components/nodes/AppropriationNode.tsx @@ -1,11 +1,13 @@ import { Handle, Node, NodeProps, Position } from "@xyflow/react"; import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle"; +import { useTranslation } from "react-i18next"; type AppropriationNodeData = { value: string }; export default function AppropriationNode({ data, }: NodeProps) { + const { t } = useTranslation(); return (
{" "} - ПРИСВОЕНИЕ + /> + {t("appropriationNode")}
) { +export default function IfElseNode({ id, data }: NodeProps) { + const { t } = useTranslation(); return ( -
+ <>
- if else logo{" "} - ЕСЛИ - ТО -
-
+
+ if else logo + {t("ifElseNode")} +
+
-
- Если {data.condition as string}, то -
-
+
+ {t("conditionIf", { condition: data.condition })} +
+
-
- Иначе -
+
+ {t("conditionElse")} +
- - - { - console.log("click"); - }} - type="source" - position={Position.Bottom} - id="false" - style={{ ...DEFAULT_HANDLE_STYLE }} - /> -
+ + + +
+ ); }