feat(client): move components
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: 5,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>{data.label}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
<div>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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<string | null>(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 handleClick = (e: React.MouseEvent, node: Node) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
const handleElement = target.closest(".react-flow__handle") as HTMLElement;
|
||||
|
||||
if (!handleElement) return;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const newEdge = {
|
||||
...con,
|
||||
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,
|
||||
id: `e${con.source}-${con.target}`,
|
||||
label: "A1",
|
||||
};
|
||||
|
||||
setEdges((eds) => [...eds, newEdge]);
|
||||
},
|
||||
[edges, setEdges]
|
||||
);
|
||||
setMenuVisible(false);
|
||||
setSelectedHandleId(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Nodes changed:", nodes);
|
||||
}, [nodes]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Edges changed:", edges);
|
||||
}, [edges]);
|
||||
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 (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
// onNodesChange={onNodesChange}
|
||||
// onEdgesChange={onEdgesChange}
|
||||
// onConnect={onConnect}
|
||||
nodesDraggable={false}
|
||||
elementsSelectable={false}
|
||||
nodesConnectable={false}
|
||||
onNodeClick={(e) => {
|
||||
console.log(e, "node clicked");
|
||||
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
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<Background color="#F2F2F2" />
|
||||
<Controls
|
||||
position="bottom-center"
|
||||
orientation="horizontal"
|
||||
showInteractive={false}
|
||||
/>
|
||||
|
||||
{menuVisible && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${menuPosition.x}px`,
|
||||
top: `${menuPosition.y}px`,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
open={menuVisible}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="bottom"
|
||||
>
|
||||
<div style={{ width: 1, height: 1 }} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
|
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={closeDrawer}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/arrow_back.svg"
|
||||
alt="close_drawer"
|
||||
style={{ height: "16px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12, flex: 1 }}>
|
||||
<Avatar
|
||||
src={
|
||||
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||
}
|
||||
size={40}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{ display: "block", fontSize: "20px" }}
|
||||
>
|
||||
{name} {login === user?.login ? t("you") : ""}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||
{email}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/delete.svg"
|
||||
alt="delete"
|
||||
style={{ height: "18px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserCreateDrawerTitle = ({
|
||||
closeDrawer,
|
||||
t,
|
||||
}: UserCreateDrawerTitleProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={closeDrawer}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/arrow_back.svg"
|
||||
alt="close_drawer"
|
||||
style={{ height: "16px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
flex: 1,
|
||||
fontSize: "20px",
|
||||
}}
|
||||
>
|
||||
{t("newAccount")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
}}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/delete.svg"
|
||||
alt="delete"
|
||||
style={{ height: "18px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeEditDrawerTitle = ({ closeDrawer, t }: NodeEditDrawerTitleProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={closeDrawer}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/arrow_back.svg"
|
||||
alt="close_drawer"
|
||||
style={{ height: "16px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
flex: 1,
|
||||
fontSize: "20px",
|
||||
}}
|
||||
>
|
||||
{t("editNode")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
}}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/delete.svg"
|
||||
alt="delete"
|
||||
style={{ height: "18px", width: "16px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UserEditDrawerTitle, UserCreateDrawerTitle, NodeEditDrawerTitle };
|
@@ -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<Node & AppropriationNodeData>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -30,8 +32,8 @@ export default function AppropriationNode({
|
||||
style={{ height: "24px", width: "24px" }}
|
||||
src="/icons/node/calculate.svg"
|
||||
alt="appropriation logo"
|
||||
/>{" "}
|
||||
ПРИСВОЕНИЕ
|
||||
/>
|
||||
{t("appropriationNode")}
|
||||
</div>
|
||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||
<div
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { Handle, NodeProps, Position, Node } from "@xyflow/react";
|
||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type IfElseNodeData = {
|
||||
interface IfElseNodeProps extends Node {
|
||||
condition: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
||||
export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
border: "0px solid",
|
||||
@@ -31,8 +34,8 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
||||
style={{ height: "24px", width: "24px" }}
|
||||
src="/icons/node/ifElse.svg"
|
||||
alt="if else logo"
|
||||
/>{" "}
|
||||
ЕСЛИ - ТО
|
||||
/>
|
||||
{t("ifElseNode")}
|
||||
</div>
|
||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||
|
||||
@@ -45,7 +48,7 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
||||
height: "48px",
|
||||
}}
|
||||
>
|
||||
Если {data.condition as string}, то
|
||||
{t("conditionIf", { condition: data.condition })}
|
||||
</div>
|
||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||
|
||||
@@ -58,25 +61,23 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
||||
height: "48px",
|
||||
}}
|
||||
>
|
||||
Иначе
|
||||
{t("conditionElse")}
|
||||
</div>
|
||||
|
||||
<Handle type="target" position={Position.Top} id="input" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="true"
|
||||
id="1"
|
||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||
/>
|
||||
<Handle
|
||||
onClick={() => {
|
||||
console.log("click");
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="false"
|
||||
id="2"
|
||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user