mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 13:39:53 +00:00
improve node clone
This commit is contained in:
parent
c713d4705e
commit
dd0d477484
@ -11,7 +11,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Dropdown } from "antd";
|
import { Dropdown } from "antd";
|
||||||
|
|
||||||
import { WorkflowNodeType, newNode } from "@/domain/workflow";
|
import { WorkflowNodeType, hasCloneNode, newNode } from "@/domain/workflow";
|
||||||
import { useZustandShallowSelector } from "@/hooks";
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -22,7 +22,9 @@ export type AddNodeProps = SharedNodeProps;
|
|||||||
const AddNode = ({ node, disabled }: AddNodeProps) => {
|
const AddNode = ({ node, disabled }: AddNodeProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"]));
|
const { addNode, workflow } = useWorkflowStore(useZustandShallowSelector(["addNode", "workflow"]));
|
||||||
|
|
||||||
|
const cloning = hasCloneNode(workflow.draft!);
|
||||||
|
|
||||||
const dropdownMenus = useMemo(() => {
|
const dropdownMenus = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -55,16 +57,29 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
|
|||||||
});
|
});
|
||||||
}, [node.id, disabled, node.type]);
|
}, [node.id, disabled, node.type]);
|
||||||
|
|
||||||
|
const renderButton = () => {
|
||||||
|
const buttonClassName =
|
||||||
|
"relative z-[1] flex size-5 items-center justify-center rounded-full " +
|
||||||
|
(cloning ? "bg-stone-300 cursor-not-allowed" : "bg-stone-400 cursor-pointer hover:bg-stone-500");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative py-6 before:absolute before:left-1/2 before:top-0 before:h-full before:w-[2px] before:-translate-x-1/2 before:bg-stone-200 before:content-['']">
|
<div className={buttonClassName}>
|
||||||
<Dropdown menu={{ items: dropdownMenus }} trigger={["click"]}>
|
|
||||||
<div className="relative z-[1] flex size-5 cursor-pointer items-center justify-center rounded-full bg-stone-400 hover:bg-stone-500">
|
|
||||||
<PlusOutlinedIcon className="text-white" />
|
<PlusOutlinedIcon className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative py-6 before:absolute before:left-1/2 before:top-0 before:h-full before:w-[2px] before:-translate-x-1/2 before:bg-stone-200 before:content-['']">
|
||||||
|
{cloning ? (
|
||||||
|
<>{renderButton()}</>
|
||||||
|
) : (
|
||||||
|
<Dropdown menu={{ items: dropdownMenus }} trigger={["click"]} disabled={disabled}>
|
||||||
|
{renderButton()}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(AddNode);
|
export default memo(AddNode);
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SharedNode.Block node={node} disabled={disabled} onClick={() => setDrawerOpen(true)}>
|
<SharedNode.Block node={node} disabled={disabled} onClick={() => {setDrawerOpen(true)}}>
|
||||||
{wrappedEl}
|
{wrappedEl}
|
||||||
</SharedNode.Block>
|
</SharedNode.Block>
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { memo } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, theme } from "antd";
|
import { Button, theme } from "antd";
|
||||||
|
|
||||||
import { type WorkflowNode } from "@/domain/workflow";
|
import { hasCloneNode, type WorkflowNode } from "@/domain/workflow";
|
||||||
import { useZustandShallowSelector } from "@/hooks";
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -15,7 +15,8 @@ export type BrandNodeProps = SharedNodeProps;
|
|||||||
const BranchNode = ({ node, disabled }: BrandNodeProps) => {
|
const BranchNode = ({ node, disabled }: BrandNodeProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { addBranch } = useWorkflowStore(useZustandShallowSelector(["addBranch"]));
|
const { addBranch, workflow } = useWorkflowStore(useZustandShallowSelector(["addBranch", "workflow"]));
|
||||||
|
const cloning = hasCloneNode(workflow.draft!);
|
||||||
|
|
||||||
const { token: themeToken } = theme.useToken();
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
@ -46,6 +47,9 @@ const BranchNode = ({ node, disabled }: BrandNodeProps) => {
|
|||||||
shape="round"
|
shape="round"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (cloning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
addBranch(node.id);
|
addBranch(node.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
import { type SharedNodeProps } from "./_SharedNode";
|
import { type SharedNodeProps } from "./_SharedNode";
|
||||||
import AddNode from "./AddNode";
|
import AddNode from "./AddNode";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export type UploadNodeProps = SharedNodeProps;
|
export type UploadNodeProps = SharedNodeProps;
|
||||||
const CloneNode = ({ node, disabled }: SharedNodeProps) => {
|
const CloneNode = ({ node, disabled }: SharedNodeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="relative z-[1] w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
<Card className="relative z-[1] w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
||||||
<div className="flex h-[64px] flex-col items-center justify-center truncate px-4 py-2">选择节点复制到此处</div>
|
<div className="flex h-[64px] flex-col items-center justify-center truncate px-4 py-2">{t("workflow_node.clone.description")}</div>
|
||||||
</Card>
|
</Card>
|
||||||
<AddNode node={node} disabled={disabled} />
|
<AddNode node={node} disabled={disabled} />
|
||||||
</>
|
</>
|
||||||
@ -15,4 +17,3 @@ const CloneNode = ({ node, disabled }: SharedNodeProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default CloneNode;
|
export default CloneNode;
|
||||||
|
|
||||||
|
@ -4,6 +4,9 @@ import { Button, Card, Popover } from "antd";
|
|||||||
|
|
||||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||||
import AddNode from "./AddNode";
|
import AddNode from "./AddNode";
|
||||||
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
import { hasCloneNode } from "@/domain/workflow";
|
||||||
|
|
||||||
export type ConditionNodeProps = SharedNodeProps & {
|
export type ConditionNodeProps = SharedNodeProps & {
|
||||||
branchId: string;
|
branchId: string;
|
||||||
@ -12,6 +15,8 @@ export type ConditionNodeProps = SharedNodeProps & {
|
|||||||
|
|
||||||
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => {
|
||||||
// TODO: 条件分支
|
// TODO: 条件分支
|
||||||
|
const { workflow } = useWorkflowStore(useZustandShallowSelector(["workflow"]));
|
||||||
|
const cloning = hasCloneNode(workflow.draft!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -29,6 +34,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
|
trigger={cloning ? [] : ["hover"]}
|
||||||
>
|
>
|
||||||
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
||||||
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { memo, useRef } from "react";
|
import { memo, useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||||
FormOutlined as FormOutlinedIcon,
|
FormOutlined as FormOutlinedIcon,
|
||||||
MoreOutlined as MoreOutlinedIcon,
|
MoreOutlined as MoreOutlinedIcon,
|
||||||
|
CopyOutlined as CopyOutlinedIcon,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useControllableValue } from "ahooks";
|
import { useControllableValue } from "ahooks";
|
||||||
import { Button, Card, Drawer, Dropdown, Input, type InputRef, Modal, Popover, Space } from "antd";
|
import { Button, Card, Drawer, Dropdown, Input, type InputRef, Modal, Popover, Space } from "antd";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { isEqual } from "radash";
|
import { isEqual } from "radash";
|
||||||
|
|
||||||
import { type WorkflowNode, WorkflowNodeType } from "@/domain/workflow";
|
import { hasCloneNode, ifCanBeCloned, type WorkflowNode, WorkflowNodeType } from "@/domain/workflow";
|
||||||
import { useZustandShallowSelector } from "@/hooks";
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -177,6 +178,10 @@ type SharedNodeBlockProps = SharedNodeProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockProps) => {
|
const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockProps) => {
|
||||||
|
const { workflow, cloneNode } = useWorkflowStore(useZustandShallowSelector(["workflow", "cloneNode"]));
|
||||||
|
const cloning = hasCloneNode(workflow.draft!);
|
||||||
|
const canBeCloned = ifCanBeCloned(node);
|
||||||
|
|
||||||
const handleNodeClick = (e: React.MouseEvent) => {
|
const handleNodeClick = (e: React.MouseEvent) => {
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
};
|
};
|
||||||
@ -189,8 +194,20 @@ const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockP
|
|||||||
arrow={false}
|
arrow={false}
|
||||||
content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} />}
|
content={<SharedNodeMenu node={node} disabled={disabled} trigger={<Button color="primary" icon={<MoreOutlinedIcon />} variant="text" />} />}
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
|
trigger={cloning ? [] : ["hover"]}
|
||||||
>
|
>
|
||||||
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable>
|
||||||
|
{cloning && canBeCloned && (
|
||||||
|
<div
|
||||||
|
className="absolute left-2 top-2 z-10 flex items-center justify-center rounded-full bg-white/90 p-1 text-primary shadow-sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
cloneNode(node);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyOutlinedIcon className="text-sm text-gray-500 hover:text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-primary flex h-[48px] flex-col items-center justify-center truncate px-4 py-2 text-white">
|
<div className="bg-primary flex h-[48px] flex-col items-center justify-center truncate px-4 py-2 text-white">
|
||||||
<SharedNodeTitle
|
<SharedNodeTitle
|
||||||
className="focus:bg-background focus:text-foreground overflow-hidden outline-none focus:rounded-sm"
|
className="focus:bg-background focus:text-foreground overflow-hidden outline-none focus:rounded-sm"
|
||||||
@ -236,6 +253,9 @@ const SharedNodeConfigDrawer = ({
|
|||||||
}: SharedNodeEditDrawerProps) => {
|
}: SharedNodeEditDrawerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { workflow } = useWorkflowStore(useZustandShallowSelector(["workflow"]));
|
||||||
|
const cloning = hasCloneNode(workflow.draft!);
|
||||||
|
|
||||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||||
|
|
||||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||||
@ -244,15 +264,28 @@ const SharedNodeConfigDrawer = ({
|
|||||||
trigger: "onOpenChange",
|
trigger: "onOpenChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && cloning) {
|
||||||
|
safeSetOpen(false);
|
||||||
|
}
|
||||||
|
}, [open, cloning]);
|
||||||
|
|
||||||
|
const safeSetOpen = (value: boolean) => {
|
||||||
|
if (value && cloning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmClick = async () => {
|
const handleConfirmClick = async () => {
|
||||||
await onConfirm();
|
await onConfirm();
|
||||||
setOpen(false);
|
safeSetOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelClick = () => {
|
const handleCancelClick = () => {
|
||||||
if (pending) return;
|
if (pending) return;
|
||||||
|
|
||||||
setOpen(false);
|
safeSetOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -275,7 +308,7 @@ const SharedNodeConfigDrawer = ({
|
|||||||
resolve(void 0);
|
resolve(void 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then(() => setOpen(false));
|
promise.then(() => safeSetOpen(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -308,7 +341,7 @@ const SharedNodeConfigDrawer = ({
|
|||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
maskClosable={!pending}
|
maskClosable={!pending}
|
||||||
open={open}
|
open={open && !cloning}
|
||||||
title={<div className="max-w-[480px] truncate">{node.name}</div>}
|
title={<div className="max-w-[480px] truncate">{node.name}</div>}
|
||||||
width={720}
|
width={720}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
@ -326,3 +359,4 @@ export default {
|
|||||||
Block: memo(SharedNodeBlock),
|
Block: memo(SharedNodeBlock),
|
||||||
ConfigDrawer: memo(SharedNodeConfigDrawer),
|
ConfigDrawer: memo(SharedNodeConfigDrawer),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,6 +42,13 @@ export enum WorkflowNodeType {
|
|||||||
Clone = "clone",
|
Clone = "clone",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowNodeTypesCanBeCloned: Set<WorkflowNodeType> = new Set([
|
||||||
|
WorkflowNodeType.Apply,
|
||||||
|
WorkflowNodeType.Upload,
|
||||||
|
WorkflowNodeType.Deploy,
|
||||||
|
WorkflowNodeType.Notify,
|
||||||
|
]);
|
||||||
|
|
||||||
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
||||||
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
|
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
|
||||||
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
|
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
|
||||||
@ -549,3 +556,42 @@ export const removeCloneNode = (node: WorkflowNode): WorkflowNode => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const cloneNode = (node: WorkflowNode, srcNode: WorkflowNode): WorkflowNode => {
|
||||||
|
// 1.先深度克隆一下 srcNode
|
||||||
|
// 2.打到 clone 节点
|
||||||
|
// 3.替换为深度克隆过的 srcNode
|
||||||
|
|
||||||
|
return produce(node, (draft) => {
|
||||||
|
let current = draft as typeof draft | undefined;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
if (current.next?.type === WorkflowNodeType.Clone) {
|
||||||
|
const clonedSrcNode = produce(srcNode, (draft) => {
|
||||||
|
draft.id = nanoid();
|
||||||
|
return draft;
|
||||||
|
});
|
||||||
|
clonedSrcNode.next = current.next?.next;
|
||||||
|
current.next = clonedSrcNode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBranchLike(current)) {
|
||||||
|
current.branches ??= [];
|
||||||
|
current.branches = current.branches.map((branch) => cloneNode(branch, srcNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.next as WorkflowNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ifCanBeCloned = (node: WorkflowNode): boolean => {
|
||||||
|
if (workflowNodeTypesCanBeCloned.has(node.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ -833,6 +833,10 @@
|
|||||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
|
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
|
||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||||
|
|
||||||
|
"workflow_node.clone.label": "Clone node",
|
||||||
|
"workflow_node.clone.description": "Select a node to clone here",
|
||||||
|
"workflow_node.clone.alert": "Select a node to copy to the target location",
|
||||||
|
|
||||||
"workflow_node.end.label": "End",
|
"workflow_node.end.label": "End",
|
||||||
|
|
||||||
"workflow_node.branch.label": "Parallel branch",
|
"workflow_node.branch.label": "Parallel branch",
|
||||||
|
@ -832,6 +832,10 @@
|
|||||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
|
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
|
||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||||
|
|
||||||
|
"workflow_node.clone.label": "复制节点",
|
||||||
|
"workflow_node.clone.description":"选择节点复制到此处",
|
||||||
|
"workflow_node.clone.alert": "选择要复制的节点,复制到目标位置",
|
||||||
|
|
||||||
"workflow_node.end.label": "结束",
|
"workflow_node.end.label": "结束",
|
||||||
|
|
||||||
"workflow_node.branch.label": "并行分支",
|
"workflow_node.branch.label": "并行分支",
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||||
HistoryOutlined as HistoryOutlinedIcon,
|
HistoryOutlined as HistoryOutlinedIcon,
|
||||||
UndoOutlined as UndoOutlinedIcon,
|
UndoOutlined as UndoOutlinedIcon,
|
||||||
|
CloseOutlined as CloseOutlinedIcon,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { Alert, Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } from "antd";
|
import { Alert, Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } from "antd";
|
||||||
@ -84,7 +85,7 @@ const WorkflowDetail = () => {
|
|||||||
const hasReleased = !!workflow.content;
|
const hasReleased = !!workflow.content;
|
||||||
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
||||||
setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges);
|
setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges);
|
||||||
setAllowRelease(!isPendingOrRunning && hasChanges);
|
setAllowRelease(!isPendingOrRunning && hasChanges && !cloning);
|
||||||
setAllowRun(hasReleased);
|
setAllowRun(hasReleased);
|
||||||
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
|
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
|
||||||
|
|
||||||
@ -316,17 +317,18 @@ const WorkflowDetail = () => {
|
|||||||
<Alert
|
<Alert
|
||||||
className="shadow-lg animate-fadeIn"
|
className="shadow-lg animate-fadeIn"
|
||||||
showIcon
|
showIcon
|
||||||
message="选择要复制的节点,复制到目标位置"
|
message={t("workflow_node.clone.alert")}
|
||||||
type="info"
|
type="info"
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="default"
|
||||||
|
icon={<CloseOutlinedIcon />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
cancelClone();
|
cancelClone();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消
|
{t("common.button.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -420,4 +422,3 @@ const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default WorkflowDetail;
|
export default WorkflowDetail;
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
type WorkflowNodeConfigForStart,
|
type WorkflowNodeConfigForStart,
|
||||||
addBranch,
|
addBranch,
|
||||||
addNode,
|
addNode,
|
||||||
|
cloneNode,
|
||||||
getOutputBeforeNodeId,
|
getOutputBeforeNodeId,
|
||||||
removeBranch,
|
removeBranch,
|
||||||
removeCloneNode,
|
removeCloneNode,
|
||||||
@ -30,6 +31,7 @@ export type WorkflowState = {
|
|||||||
updateNode: (node: WorkflowNode) => void;
|
updateNode: (node: WorkflowNode) => void;
|
||||||
removeNode: (nodeId: string) => void;
|
removeNode: (nodeId: string) => void;
|
||||||
cancelClone: () => void;
|
cancelClone: () => void;
|
||||||
|
cloneNode: (node: WorkflowNode) => void;
|
||||||
|
|
||||||
addBranch: (branchId: string) => void;
|
addBranch: (branchId: string) => void;
|
||||||
removeBranch: (branchId: string, index: number) => void;
|
removeBranch: (branchId: string, index: number) => void;
|
||||||
@ -226,6 +228,26 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cloneNode: async (node: WorkflowNode) => {
|
||||||
|
if (!get().initialized) throw "Workflow not initialized yet";
|
||||||
|
|
||||||
|
const root = cloneNode(get().workflow.draft!, node);
|
||||||
|
const resp = await saveWorkflow({
|
||||||
|
id: get().workflow.id!,
|
||||||
|
draft: root,
|
||||||
|
hasDraft: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
set((state: WorkflowState) => {
|
||||||
|
return {
|
||||||
|
workflow: produce(state.workflow, (draft) => {
|
||||||
|
draft.draft = resp.draft;
|
||||||
|
draft.hasDraft = resp.hasDraft;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
addBranch: async (branchId: string) => {
|
addBranch: async (branchId: string) => {
|
||||||
if (!get().initialized) throw "Workflow not initialized yet";
|
if (!get().initialized) throw "Workflow not initialized yet";
|
||||||
|
|
||||||
@ -270,4 +292,3 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user