mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat: import/export workflow graph
This commit is contained in:
@@ -273,7 +273,6 @@ func newInOutManager() InOutManager {
|
||||
|
||||
const (
|
||||
stateIOTypeRef = "ref"
|
||||
stateIOType = "recordRef"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
Generated
+346
-348
File diff suppressed because it is too large
Load Diff
+15
-14
@@ -13,20 +13,20 @@
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@flowgram.ai/document": "^0.4.0",
|
||||
"@flowgram.ai/fixed-layout-editor": "^0.4.0",
|
||||
"@flowgram.ai/minimap-plugin": "^0.4.0",
|
||||
"@flowgram.ai/document": "0.4.3",
|
||||
"@flowgram.ai/fixed-layout-editor": "0.4.3",
|
||||
"@flowgram.ai/minimap-plugin": "0.4.3",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@uiw/codemirror-extensions-basic-setup": "^4.25.1",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.1",
|
||||
"@uiw/react-codemirror": "^4.25.1",
|
||||
"ahooks": "^3.9.0",
|
||||
"antd": "^5.27.0",
|
||||
"ahooks": "^3.9.4",
|
||||
"antd": "^5.27.1",
|
||||
"antd-zod": "^7.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-parser": "^5.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.3.6",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immer": "^10.1.1",
|
||||
"nanoid": "^5.1.5",
|
||||
@@ -35,14 +35,15 @@
|
||||
"react": "^18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-router-dom": "^7.8.1",
|
||||
"react-i18next": "^15.7.2",
|
||||
"react-router-dom": "^7.8.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.17",
|
||||
"zustand": "^5.0.7"
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.1.1",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
@@ -53,10 +54,10 @@
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-better-tailwindcss": "^3.7.4",
|
||||
"eslint-plugin-better-tailwindcss": "^3.7.5",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
@@ -65,7 +66,7 @@
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ const ModalForm = <T extends NonNullable<unknown> = any>({
|
||||
onOk={handleOkClick}
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="pt-4 pb-2">
|
||||
<div className="py-3">
|
||||
<Form className={className} style={style} {...mergedFormProps} form={formInst}>
|
||||
{children}
|
||||
</Form>
|
||||
|
||||
@@ -146,7 +146,7 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
|
||||
const useDrawer = () => {
|
||||
type DataType = AccessEditDrawerProps["data"];
|
||||
const [data, setData] = useState<DataType>();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
|
||||
@@ -65,7 +65,7 @@ const CertificateDetailDrawer = ({ afterClose, data, loading, trigger, ...props
|
||||
const useDrawer = () => {
|
||||
type DataType = CertificateDetailDrawerProps["data"];
|
||||
const [data, setData] = useState<DataType>();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FlowNodeBaseType, FlowNodeSplitType } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconClipboard } from "@tabler/icons-react";
|
||||
import { App, Button, Form, Radio, Tooltip } from "antd";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
|
||||
import CodeInput from "@/components/CodeInput";
|
||||
import { type WorkflowGraph, type WorkflowNode } from "@/domain/workflow";
|
||||
|
||||
import { getAllNodeRegistries } from "./designer/nodes";
|
||||
|
||||
export type WorkflowGraphExportBoxFormats = "json" | "yaml";
|
||||
|
||||
export interface WorkflowGraphExportBoxProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
data: WorkflowGraph;
|
||||
}
|
||||
|
||||
const serialize = (graph: WorkflowGraph | undefined, format: WorkflowGraphExportBoxFormats): string | undefined => {
|
||||
if (!graph) return;
|
||||
|
||||
const nodeRegistries = getAllNodeRegistries();
|
||||
|
||||
const deepConvert = (node: WorkflowNode): Map<string, unknown> => {
|
||||
// 利用 Map 来保证字段序列化的有序性
|
||||
const map = new Map<string, unknown>([
|
||||
["id", node.id],
|
||||
["type", node.type],
|
||||
["name", node.data.name],
|
||||
]);
|
||||
|
||||
if (node.data.disabled != null) {
|
||||
if (node.data.disabled) {
|
||||
map.set("disabled", node.data.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.config != null) {
|
||||
map.set("config", node.data.config);
|
||||
}
|
||||
|
||||
if (node.blocks != null) {
|
||||
const branchLikeNodeTypes = [
|
||||
FlowNodeBaseType.BLOCK,
|
||||
FlowNodeSplitType.SIMPLE_SPLIT,
|
||||
FlowNodeSplitType.DYNAMIC_SPLIT,
|
||||
FlowNodeSplitType.STATIC_SPLIT,
|
||||
] as string[];
|
||||
const hasChildren = node.blocks.length > 0 || branchLikeNodeTypes.includes(nodeRegistries.find((r) => r.type === node.type)?.extend ?? "");
|
||||
if (hasChildren) {
|
||||
const childern = node.blocks.map((block) => deepConvert(block));
|
||||
map.set("blocks", childern);
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(node.data).forEach(([k, v]) => {
|
||||
if (k === "name" || k === "disabled" || k === "config") return;
|
||||
map.set(k, v);
|
||||
});
|
||||
|
||||
return map;
|
||||
};
|
||||
const nodes = graph.nodes.map((node) => deepConvert(node));
|
||||
|
||||
let content: string = "";
|
||||
switch (format) {
|
||||
case "json":
|
||||
content = JSON.stringify(
|
||||
{ ...graph, nodes },
|
||||
(_, value) => {
|
||||
if (value instanceof Map) {
|
||||
return Object.fromEntries(value.entries());
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
2
|
||||
);
|
||||
break;
|
||||
|
||||
case "yaml":
|
||||
content = stringifyYaml(
|
||||
{ ...graph, nodes },
|
||||
{
|
||||
indent: 2,
|
||||
defaultKeyType: "PLAIN",
|
||||
defaultStringType: "QUOTE_DOUBLE",
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const WorkflowGraphExportBox = ({ className, style, data }: WorkflowGraphExportBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [format, setFormat] = useState<WorkflowGraphExportBoxFormats>("yaml");
|
||||
const [content, setContent] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setContent(serialize(data, format));
|
||||
}, [data, format]);
|
||||
|
||||
return (
|
||||
<Form className={className} style={style} layout="vertical">
|
||||
<Form.Item className="mb-4" label={t("workflow.detail.design.action.export.form.format.label")}>
|
||||
<Radio.Group className="w-full" value={format} onChange={(e) => setFormat(e.target.value)}>
|
||||
<Radio.Button className="w-1/2 text-center" value="yaml">
|
||||
YAML
|
||||
</Radio.Button>
|
||||
<Radio.Button className="w-1/2 text-center" value="json">
|
||||
JSON
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("workflow.detail.design.action.export.form.content.label")}>
|
||||
<div className="absolute -top-[6px] right-0 -translate-y-full">
|
||||
<Tooltip title={t("common.button.copy")}>
|
||||
<CopyToClipboard
|
||||
text={content!}
|
||||
onCopy={() => {
|
||||
message.success(t("common.text.copied"));
|
||||
}}
|
||||
>
|
||||
<Button icon={<IconClipboard size="1.25em" />} disabled={!content} size="small" type="text" />
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<CodeInput height="calc(min(60vh, 512px))" language={format} value={content} readOnly />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowGraphExportBox;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { startTransition, useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Modal } from "antd";
|
||||
|
||||
import { type WorkflowGraph } from "@/domain/workflow";
|
||||
import { useTriggerElement } from "@/hooks";
|
||||
|
||||
import WorkflowGraphExportBox from "./WorkflowGraphExportBox";
|
||||
|
||||
export interface WorkflowGraphExportModalProps {
|
||||
afterClose?: () => void;
|
||||
data: WorkflowGraph;
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const WorkflowGraphExportModal = ({ afterClose, data, loading, trigger, ...props }: WorkflowGraphExportModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
defaultValuePropName: "defaultOpen",
|
||||
trigger: "onOpenChange",
|
||||
});
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
|
||||
|
||||
const handleCancelClick = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{triggerEl}
|
||||
|
||||
<Modal
|
||||
afterClose={afterClose}
|
||||
closable
|
||||
destroyOnHidden
|
||||
footer={null}
|
||||
loading={loading}
|
||||
open={open}
|
||||
title={t("workflow.detail.design.action.export.modal.title")}
|
||||
width="768px"
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="py-3 pb-0">
|
||||
<WorkflowGraphExportBox data={data} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useModal = () => {
|
||||
type DataType = WorkflowGraphExportModalProps["data"];
|
||||
const [data, setData] = useState<DataType>();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
modalProps: {
|
||||
afterClose: () => {
|
||||
startTransition(() => {
|
||||
if (!open) {
|
||||
setData(void 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
data: data!,
|
||||
open,
|
||||
onOpenChange,
|
||||
},
|
||||
|
||||
open: (data: DataType) => {
|
||||
setData(data);
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const _default = Object.assign(WorkflowGraphExportModal, {
|
||||
useModal,
|
||||
});
|
||||
|
||||
export default _default;
|
||||
@@ -0,0 +1,249 @@
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Radio } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
import CodeInput from "@/components/CodeInput";
|
||||
import { WORKFLOW_NODE_TYPES, type WorkflowGraph, type WorkflowNode, type WorkflowNodeType } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
|
||||
export type WorkflowGraphImportInputBoxFormats = "json" | "yaml";
|
||||
|
||||
export interface WorkflowGraphImportInputBoxProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface WorkflowGraphImportInputBoxInstance {
|
||||
validate: () => Promise<WorkflowGraph | undefined>;
|
||||
}
|
||||
|
||||
const deserialize = (content: string | undefined, format: WorkflowGraphImportInputBoxFormats): WorkflowGraph | undefined => {
|
||||
if (!content?.trim()) return;
|
||||
|
||||
let temp: any;
|
||||
switch (format) {
|
||||
case "json":
|
||||
temp = JSON.parse(content);
|
||||
break;
|
||||
|
||||
case "yaml":
|
||||
temp = parseYaml(content);
|
||||
break;
|
||||
}
|
||||
|
||||
const deepParse = (item: any): WorkflowNode => {
|
||||
item = item ?? {};
|
||||
|
||||
const node: WorkflowNode = {
|
||||
id: item.id?.toString() ?? "",
|
||||
type: (item.type?.toString() ?? "") as WorkflowNodeType,
|
||||
data: {
|
||||
name: item.name?.toString() ?? "",
|
||||
disabled: item.disabled === true || item.disabled === "true",
|
||||
config: item.config,
|
||||
},
|
||||
blocks: Array.isArray(item.blocks) ? item.blocks.map((block: any) => deepParse(block)) : [],
|
||||
};
|
||||
|
||||
if (item.data != null) {
|
||||
Object.entries(item.data).forEach(([k, v]) => {
|
||||
if (k === "id" || k === "type" || k === "meta" || k === "blocks") return;
|
||||
if (k === "name" || k === "disabled" || k === "config") return;
|
||||
node.data[k] = v;
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
const nodes = Array.from(temp.nodes ?? []).map((item) => deepParse(item));
|
||||
|
||||
return { nodes: nodes };
|
||||
};
|
||||
|
||||
const WorkflowGraphImportInputBox = forwardRef<WorkflowGraphImportInputBoxInstance, WorkflowGraphImportInputBoxProps>(({ className, style }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
format: z.enum(["json", "yaml"]),
|
||||
content: z.string().refine((v) => !!v?.trim(), t("workflow.detail.design.action.import.form.content.errmsg.invalid")),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
let graph: WorkflowGraph | undefined;
|
||||
try {
|
||||
graph = deserialize(values.content, values.format);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow.detail.design.action.import.form.content.errmsg.invalid"),
|
||||
path: ["content"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (graph) {
|
||||
const errmsgs: string[] = [];
|
||||
|
||||
if (graph.nodes.at(0)?.type !== WORKFLOW_NODE_TYPES.START) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.first_node_start"));
|
||||
}
|
||||
|
||||
if (graph.nodes.at(-1)?.type !== WORKFLOW_NODE_TYPES.END) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.last_node_end"));
|
||||
}
|
||||
|
||||
let startNodeId: string | undefined;
|
||||
let startNodeDuplicated: boolean = false;
|
||||
const nodeIds = new Set<string>();
|
||||
const deepValidate = (node: WorkflowNode) => {
|
||||
// 验证字段:ID
|
||||
if (!/^(?![_-])[a-zA-Z0-9_-]{1,32}$/.test(node.id)) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.invalid_id", { nodeId: node.id }));
|
||||
}
|
||||
|
||||
// 验证字段:配置项
|
||||
if (node.data.config != null) {
|
||||
if (typeof node.data.config !== "object" || Array.isArray(node.data.config)) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.invalid_config", { nodeId: node.id }));
|
||||
}
|
||||
}
|
||||
|
||||
// 验证节点 ID 是否冲突
|
||||
if (nodeIds.has(node.id)) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.conflict_id", { nodeId: node.id }));
|
||||
} else {
|
||||
nodeIds.add(node.id);
|
||||
}
|
||||
|
||||
// 验证开始节点是否重复
|
||||
if (node.type === WORKFLOW_NODE_TYPES.START) {
|
||||
if (startNodeId) {
|
||||
if (!startNodeDuplicated) {
|
||||
startNodeDuplicated = true;
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.duplicate_start"));
|
||||
}
|
||||
} else {
|
||||
startNodeId = node.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 Condition 分支结构
|
||||
if (node.type === WORKFLOW_NODE_TYPES.CONDITION) {
|
||||
const blocks = node.blocks ?? [];
|
||||
const f1 = blocks.length > 0;
|
||||
const f2 = blocks.every((block) => block.type === WORKFLOW_NODE_TYPES.BRANCHBLOCK);
|
||||
if (!f1 || !f2) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch", { nodeId: node.id }));
|
||||
}
|
||||
} else if (node.type === WORKFLOW_NODE_TYPES.BRANCHBLOCK) {
|
||||
const blocks = node.blocks ?? [];
|
||||
const f1 = blocks.length > 0;
|
||||
const f2 = blocks.every((block) => block.type !== WORKFLOW_NODE_TYPES.BRANCHBLOCK);
|
||||
if (!f1 || !f2) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch", { nodeId: node.id }));
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 TryCatch 分支结构
|
||||
if (node.type === WORKFLOW_NODE_TYPES.TRYCATCH) {
|
||||
const blocks = node.blocks ?? [];
|
||||
const f1 = blocks.length >= 2;
|
||||
const f2 = blocks[0]?.type === WORKFLOW_NODE_TYPES.TRYBLOCK;
|
||||
const f3 = blocks.some((block) => block.type === WORKFLOW_NODE_TYPES.CATCHBLOCK);
|
||||
if (!f1 || !f2 || !f3) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch", { nodeId: node.id }));
|
||||
}
|
||||
} else if (node.type === WORKFLOW_NODE_TYPES.TRYBLOCK || node.type === WORKFLOW_NODE_TYPES.CATCHBLOCK) {
|
||||
const blocks = node.blocks ?? [];
|
||||
const f1 = blocks.length > 0;
|
||||
const f2 = blocks.every((block) => block.type !== WORKFLOW_NODE_TYPES.TRYBLOCK);
|
||||
const f3 = blocks.every((block) => block.type !== WORKFLOW_NODE_TYPES.CATCHBLOCK);
|
||||
if (!f1 || !f2 || !f3) {
|
||||
errmsgs.push(t("workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch", { nodeId: node.id }));
|
||||
}
|
||||
}
|
||||
|
||||
// 验证子节点
|
||||
if (Array.isArray(node.blocks)) {
|
||||
node.blocks.forEach((block) => deepValidate(block));
|
||||
}
|
||||
};
|
||||
graph.nodes.forEach((node) => deepValidate(node));
|
||||
|
||||
const MAX_ISSUE_COUNT = 5;
|
||||
for (let i = 0; i < Math.min(MAX_ISSUE_COUNT, errmsgs.length); i++) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: errmsgs[i],
|
||||
path: ["content"],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({
|
||||
name: "workflowGraphExportInputBoxForm",
|
||||
initialValues: {
|
||||
format: "yaml",
|
||||
content: "",
|
||||
},
|
||||
});
|
||||
|
||||
const fieldFormat = Form.useWatch<WorkflowGraphImportInputBoxFormats>("format", formInst);
|
||||
const fieldContent = Form.useWatch<string>("content", formInst);
|
||||
|
||||
const handleFormatChange = (format: WorkflowGraphImportInputBoxFormats) => {
|
||||
formInst.setFieldValue("format", format);
|
||||
|
||||
switch (format) {
|
||||
case "json":
|
||||
try {
|
||||
JSON.parse(fieldContent);
|
||||
} catch {
|
||||
formInst.setFieldValue("content", "");
|
||||
}
|
||||
break;
|
||||
|
||||
case "yaml":
|
||||
try {
|
||||
parseYaml(fieldContent);
|
||||
} catch {
|
||||
formInst.setFieldValue("content", "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
validate: async () => {
|
||||
const formValues = await formInst.validateFields();
|
||||
return deserialize(formValues.content, formValues.format);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Form className={className} style={style} {...formProps} clearOnDestroy={true} form={formInst} layout="vertical" preserve={false} scrollToFirstError>
|
||||
<Form.Item className="mb-4" name="format" label={t("workflow.detail.design.action.import.form.format.label")} rules={[formRule]}>
|
||||
<Radio.Group className="w-full" onChange={(e) => handleFormatChange(e.target.value)}>
|
||||
<Radio.Button className="w-1/2 text-center" value="yaml">
|
||||
YAML
|
||||
</Radio.Button>
|
||||
<Radio.Button className="w-1/2 text-center" value="json">
|
||||
JSON
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="content" label={t("workflow.detail.design.action.import.form.content.label")} rules={[formRule]}>
|
||||
<CodeInput height="calc(min(60vh, 512px))" language={fieldFormat} value={fieldContent} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkflowGraphImportInputBox;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { startTransition, useCallback, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Flex, Modal } from "antd";
|
||||
|
||||
import { type WorkflowGraph } from "@/domain/workflow";
|
||||
import { useTriggerElement } from "@/hooks";
|
||||
|
||||
import WorkflowImportExportForm, { type WorkflowGraphImportInputBoxInstance } from "./WorkflowGraphImportInputBox";
|
||||
|
||||
export interface WorkflowGraphImportModalProps {
|
||||
afterClose?: () => void;
|
||||
open?: boolean;
|
||||
trigger?: React.ReactNode;
|
||||
onCancel?: () => void;
|
||||
onOk?: (graph: WorkflowGraph) => void;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const WorkflowGraphImportModal = ({ afterClose, trigger, onCancel, onOk, ...props }: WorkflowGraphImportModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
defaultValuePropName: "defaultOpen",
|
||||
trigger: "onOpenChange",
|
||||
});
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
|
||||
|
||||
const graphInputBoxRef = useRef<WorkflowGraphImportInputBoxInstance>(null);
|
||||
|
||||
const handleCancelClick = () => {
|
||||
setOpen(false);
|
||||
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const handleOkClick = async () => {
|
||||
const graph = await graphInputBoxRef.current!.validate();
|
||||
|
||||
setOpen(false);
|
||||
|
||||
if (graph != null) {
|
||||
onOk?.(graph);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{triggerEl}
|
||||
|
||||
<Modal
|
||||
afterClose={afterClose}
|
||||
closable
|
||||
destroyOnHidden
|
||||
footer={
|
||||
<Flex className="px-2" justify="end" gap="small">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button type="primary" onClick={handleOkClick}>
|
||||
{t("workflow.detail.design.action.import.modal.ok_button")}
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
open={open}
|
||||
title={t("workflow.detail.design.action.import.modal.title")}
|
||||
width="768px"
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="py-3">
|
||||
<WorkflowImportExportForm ref={graphInputBoxRef} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useModal = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [onOkHandler, setOnOkHandler] = useState<{ handler: WorkflowGraphImportModalProps["onOk"] }>();
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
modalProps: {
|
||||
afterClose: () => {
|
||||
startTransition(() => {
|
||||
if (!open) {
|
||||
setOnOkHandler(void 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
open,
|
||||
onOk: (graph: WorkflowGraph) => {
|
||||
onOkHandler?.handler?.(graph);
|
||||
},
|
||||
onOpenChange,
|
||||
},
|
||||
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<WorkflowGraph>();
|
||||
setOnOkHandler({ handler: (graph) => resolve(graph) });
|
||||
return promise;
|
||||
},
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const _default = Object.assign(WorkflowGraphImportModal, {
|
||||
useModal,
|
||||
});
|
||||
|
||||
export default _default;
|
||||
@@ -65,7 +65,7 @@ const WorkflowRunDetailDrawer = ({ afterClose, data, loading, trigger, ...props
|
||||
const useDrawer = () => {
|
||||
type DataType = WorkflowRunDetailDrawerProps["data"];
|
||||
const [data, setData] = useState<DataType>();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
|
||||
@@ -75,7 +75,7 @@ const NodeDrawer = ({ node, trigger, ...props }: NodeDrawerProps) => {
|
||||
const useDrawer = () => {
|
||||
type NodeDataType = NodeDrawerProps["node"];
|
||||
const [node, setNode] = useState<NodeDataType>();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
|
||||
+9
-8
@@ -21,7 +21,7 @@ import { useNodeFormContext } from "./_context";
|
||||
import { getAllPreviousNodes } from "../_util";
|
||||
import { NodeType } from "../nodes/typings";
|
||||
|
||||
export interface BranchBlockNodeConfigFormExpressionEditorProps {
|
||||
export interface BranchBlockNodeConfigExprInputBoxProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
defaultValue?: Expr;
|
||||
@@ -29,8 +29,8 @@ export interface BranchBlockNodeConfigFormExpressionEditorProps {
|
||||
onChange?: (value: Expr) => void;
|
||||
}
|
||||
|
||||
export interface BranchBlockNodeConfigFormExpressionEditorInstance {
|
||||
validate: () => Promise<void>;
|
||||
export interface BranchBlockNodeConfigExprInputBoxInstance {
|
||||
validate: () => Promise<Expr | undefined>;
|
||||
}
|
||||
|
||||
// 表单内部使用的扁平结构
|
||||
@@ -50,7 +50,7 @@ type ConditionFormValues = {
|
||||
logicalOperator: ExprLogicalOperator;
|
||||
};
|
||||
|
||||
const exprToFormValues = (expr?: Expr): ConditionFormValues => {
|
||||
const exprToFormValues = (expr: Expr | undefined): ConditionFormValues => {
|
||||
if (!expr) return getInitialValues();
|
||||
|
||||
const conditions: ConditionItem[] = [];
|
||||
@@ -138,7 +138,7 @@ const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => {
|
||||
return expr;
|
||||
};
|
||||
|
||||
const BranchBlockNodeConfigFormExpressionEditor = forwardRef<BranchBlockNodeConfigFormExpressionEditorInstance, BranchBlockNodeConfigFormExpressionEditorProps>(
|
||||
const BranchBlockNodeConfigExprInputBox = forwardRef<BranchBlockNodeConfigExprInputBoxInstance, BranchBlockNodeConfigExprInputBoxProps>(
|
||||
({ className, style, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -153,7 +153,7 @@ const BranchBlockNodeConfigFormExpressionEditor = forwardRef<BranchBlockNodeConf
|
||||
const { node } = useNodeFormContext();
|
||||
|
||||
const [formInst] = Form.useForm<ConditionFormValues>();
|
||||
const formName = useAntdFormName({ form: formInst, name: "workflowNodeBranchBlockConfigFormExpressionEditorForm" });
|
||||
const formName = useAntdFormName({ form: formInst, name: "workflowNodeBranchBlockConfigExprInputBoxForm" });
|
||||
const [formModel, setFormModel] = useState<ConditionFormValues>(getInitialValues());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -253,8 +253,9 @@ const BranchBlockNodeConfigFormExpressionEditor = forwardRef<BranchBlockNodeConf
|
||||
return {
|
||||
validate: async () => {
|
||||
await formInst.validateFields();
|
||||
return formValuesToExpr(formInst.getFieldsValue());
|
||||
},
|
||||
} as BranchBlockNodeConfigFormExpressionEditorInstance;
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -383,4 +384,4 @@ const getInitialValues = (): ConditionFormValues => {
|
||||
};
|
||||
};
|
||||
|
||||
export default BranchBlockNodeConfigFormExpressionEditor;
|
||||
export default BranchBlockNodeConfigExprInputBox;
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { useAntdForm } from "@/hooks";
|
||||
|
||||
import { NodeFormContextProvider } from "./_context";
|
||||
import BranchBlockNodeConfigFormExpressionEditor, { type BranchBlockNodeConfigFormExpressionEditorInstance } from "./BranchBlockNodeConfigFormExpressionEditor";
|
||||
import BranchBlockNodeConfigExprInputBox, { type BranchBlockNodeConfigExprInputBoxInstance } from "./BranchBlockNodeConfigExprInputBox";
|
||||
|
||||
import { NodeType } from "../nodes/typings";
|
||||
|
||||
@@ -41,7 +41,7 @@ const BranchBlockNodeConfigForm = ({ node, ...props }: BranchBlockNodeConfigForm
|
||||
const formSchema = getSchema({ i18n }).superRefine(async (values, ctx) => {
|
||||
if (values.expression != null) {
|
||||
try {
|
||||
await exprEditorRef.current!.validate();
|
||||
await exprInputBoxRef.current!.validate();
|
||||
} catch {
|
||||
if (!ctx.issues.some((issue) => issue.path?.[0] === "expression")) {
|
||||
ctx.addIssue({
|
||||
@@ -60,14 +60,14 @@ const BranchBlockNodeConfigForm = ({ node, ...props }: BranchBlockNodeConfigForm
|
||||
initialValues: initialValues ?? getInitialValues(),
|
||||
});
|
||||
|
||||
const exprEditorRef = useRef<BranchBlockNodeConfigFormExpressionEditorInstance>(null);
|
||||
const exprInputBoxRef = useRef<BranchBlockNodeConfigExprInputBoxInstance>(null);
|
||||
|
||||
return (
|
||||
<NodeFormContextProvider value={{ node }}>
|
||||
<Form {...formProps} clearOnDestroy={true} form={formInst} layout="vertical" preserve={false} scrollToFirstError>
|
||||
<div id="parameters" data-anchor="parameters">
|
||||
<Form.Item name="expression" label={t("workflow_node.branch_block.form.expression.label")} rules={[formRule]}>
|
||||
<BranchBlockNodeConfigFormExpressionEditor ref={exprEditorRef} />
|
||||
<BranchBlockNodeConfigExprInputBox ref={exprInputBoxRef} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type FlowNodeEntity, getNodeForm, useClientContext, useRefresh, useWatchFormValueIn } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { type FlowNodeEntity, getNodeForm, useClientContext, useRefresh } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconEye, IconEyeOff, IconX } from "@tabler/icons-react";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Anchor, type AnchorProps, App, Button, Drawer, Flex, type FormInstance, Tooltip, Typography } from "antd";
|
||||
@@ -41,7 +41,7 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [formPending, setFormPending] = useState<boolean>(false);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
|
||||
const nodeRegistry = node?.getNodeRegistry<NodeRegistry>();
|
||||
const NodeIcon = nodeRegistry?.meta?.icon;
|
||||
@@ -134,13 +134,12 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
const newValues = picker(formInst.getFieldsValue(true));
|
||||
const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues);
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
if (changed) {
|
||||
modal.confirm({
|
||||
title: t("common.text.operation_confirm"),
|
||||
content: t("workflow.detail.design.unsaved_changes.confirm"),
|
||||
onOk: () => resolve(void 0),
|
||||
onCancel: () => reject(),
|
||||
});
|
||||
} else {
|
||||
resolve(void 0);
|
||||
|
||||
@@ -59,6 +59,7 @@ export type WorkflowNode = {
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
blocks?: WorkflowNode[];
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"common.button.add": "Add",
|
||||
"common.button.cancel": "Cancel",
|
||||
"common.button.close": "Close",
|
||||
"common.button.confirm": "Confirm",
|
||||
"common.button.create": "Create",
|
||||
"common.button.copy": "Copy",
|
||||
"common.button.create": "Create",
|
||||
"common.button.delete": "Delete",
|
||||
"common.button.download": "Download",
|
||||
"common.button.edit": "Edit",
|
||||
@@ -23,6 +24,7 @@
|
||||
"common.text.operation_succeeded": "Operation succeeded",
|
||||
"common.text.operation_failed": "Operation failed",
|
||||
"common.text.request_error": "Request error",
|
||||
"common.text.saving": "Saving ...",
|
||||
"common.text.search": "Search ...",
|
||||
|
||||
"common.menu.document": "Document",
|
||||
|
||||
@@ -82,6 +82,24 @@
|
||||
"workflow.detail.design.action.rollback.button": "Rollback",
|
||||
"workflow.detail.design.action.rollback.modal.title": "Rollback changes",
|
||||
"workflow.detail.design.action.rollback.modal.content": "Are you sure to rollback your changes to the last published version?",
|
||||
"workflow.detail.design.action.import.button": "Import",
|
||||
"workflow.detail.design.action.import.modal.title": "Import workflow",
|
||||
"workflow.detail.design.action.import.modal.ok_button": "Import",
|
||||
"workflow.detail.design.action.import.form.format.label": "Format",
|
||||
"workflow.detail.design.action.import.form.content.label": "Content",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid": "Please enter a valid content",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.first_node_start": "The first node must be a start node",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.last_node_end": "The last node must be an end node",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.duplicate_start": "The start node must be unique",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid_id": "Node ID invalid: #{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid_config": "Node config invalid: #{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.conflict_id": "Node ID conflict: #{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch": "Abnormal parallel/conditional branch: #{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch": "Abnormal execution result branch: #{{nodeId}}",
|
||||
"workflow.detail.design.action.export.button": "Export",
|
||||
"workflow.detail.design.action.export.modal.title": "Export workflow",
|
||||
"workflow.detail.design.action.export.form.format.label": "Format",
|
||||
"workflow.detail.design.action.export.form.content.label": "Content",
|
||||
"workflow.detail.design.uncompleted_design.alert": "The orchestration is not ready. Please check if there are any nodes not configured.",
|
||||
"workflow.detail.design.unpublished_draft.alert": "The orchestration is not released yet.",
|
||||
"workflow.detail.design.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"common.button.add": "新增",
|
||||
"common.button.cancel": "取消",
|
||||
"common.button.close": "关闭",
|
||||
"common.button.confirm": "确定",
|
||||
"common.button.create": "新建",
|
||||
"common.button.copy": "复制",
|
||||
"common.button.create": "新建",
|
||||
"common.button.delete": "刪除",
|
||||
"common.button.download": "下载",
|
||||
"common.button.edit": "编辑",
|
||||
@@ -23,6 +24,7 @@
|
||||
"common.text.operation_succeeded": "操作成功",
|
||||
"common.text.operation_failed": "操作失败",
|
||||
"common.text.request_error": "请求错误",
|
||||
"common.text.saving": "保存中 ……",
|
||||
"common.text.search": "搜索 ……",
|
||||
|
||||
"common.menu.document": "文档",
|
||||
|
||||
@@ -82,6 +82,24 @@
|
||||
"workflow.detail.design.action.rollback.button": "回退更改",
|
||||
"workflow.detail.design.action.rollback.modal.title": "回退更改",
|
||||
"workflow.detail.design.action.rollback.modal.content": "确定要回退到最近一次发布的版本吗?",
|
||||
"workflow.detail.design.action.import.button": "导入",
|
||||
"workflow.detail.design.action.import.modal.title": "导入工作流",
|
||||
"workflow.detail.design.action.import.modal.ok_button": "导入",
|
||||
"workflow.detail.design.action.import.form.format.label": "格式",
|
||||
"workflow.detail.design.action.import.form.content.label": "内容",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid": "请输入有效的内容",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.first_node_start": "第一个节点必须是开始节点",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.last_node_end": "最后一个节点必须是结束节点",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.duplicate_start": "有且只能有一个开始节点",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid_id": "节点 ID 无效:#{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.invalid_config": "节点配置无效:#{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.conflict_id": "节点 ID 冲突:#{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch": "并行/条件分支结构异常:#{{nodeId}}",
|
||||
"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch": "执行结果分支结构异常:#{{nodeId}}",
|
||||
"workflow.detail.design.action.export.button": "导出",
|
||||
"workflow.detail.design.action.export.modal.title": "导出工作流",
|
||||
"workflow.detail.design.action.export.form.format.label": "格式",
|
||||
"workflow.detail.design.action.export.form.content.label": "内容",
|
||||
"workflow.detail.design.uncompleted_design.alert": "流程编排未就绪,请检查是否有节点未完成配置。",
|
||||
"workflow.detail.design.unpublished_draft.alert": "当前编排有尚未发布的更改。",
|
||||
"workflow.detail.design.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
|
||||
|
||||
@@ -65,13 +65,12 @@ const WorkflowDetail = () => {
|
||||
}, [workflow.lastRunStatus]);
|
||||
|
||||
const handleRunClick = () => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
if (workflow.hasDraft) {
|
||||
modal.confirm({
|
||||
title: t("workflow.action.run.modal.title"),
|
||||
content: t("workflow.action.run.modal.content"),
|
||||
onOk: () => resolve(void 0),
|
||||
onCancel: () => reject(),
|
||||
});
|
||||
} else {
|
||||
resolve(void 0);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconArrowBackUp, IconDots } from "@tabler/icons-react";
|
||||
import { IconArrowBackUp, IconDots, IconTransferIn, IconTransferOut } from "@tabler/icons-react";
|
||||
import { useDeepCompareEffect } from "ahooks";
|
||||
import { Alert, App, Button, Card, Dropdown, Result, Space, theme } from "antd";
|
||||
import { debounce } from "radash";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { WorkflowDesigner, type WorkflowDesignerInstance, WorkflowNodeDrawer, WorkflowToolbar } from "@/components/workflow/designer";
|
||||
import WorkflowGraphExportModal from "@/components/workflow/WorkflowGraphExportModal";
|
||||
import WorkflowGraphImportModal from "@/components/workflow/WorkflowGraphImportModal";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
@@ -93,8 +95,33 @@ const WorkflowDetailDesign = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportClick = async () => {
|
||||
graphImportModal.open().then(async (graph) => {
|
||||
const loadingKey = Math.random().toString(36).substring(0, 8);
|
||||
message.loading({ key: loadingKey, content: t("common.text.saving"), duration: 0 });
|
||||
|
||||
try {
|
||||
await workflowStore.orchestrate(graph);
|
||||
|
||||
message.destroy(loadingKey);
|
||||
message.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.destroy(loadingKey);
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleExportClick = () => {
|
||||
graphExportModal.open(workflow.graphDraft!);
|
||||
};
|
||||
|
||||
const { drawerProps: designerNodeDrawerProps, ...designerNodeDrawer } = WorkflowNodeDrawer.useDrawer();
|
||||
|
||||
const { modalProps: graphImportModalProps, ...graphImportModal } = WorkflowGraphImportModal.useModal();
|
||||
const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal();
|
||||
|
||||
return (
|
||||
<div className="size-full">
|
||||
<Card
|
||||
@@ -131,6 +158,21 @@ const WorkflowDetailDesign = () => {
|
||||
icon: <IconArrowBackUp size="1.25em" />,
|
||||
onClick: handleRollbackClick,
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: t("workflow.detail.design.action.import.button"),
|
||||
icon: <IconTransferIn size="1.25em" />,
|
||||
onClick: handleImportClick,
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: t("workflow.detail.design.action.export.button"),
|
||||
icon: <IconTransferOut size="1.25em" />,
|
||||
onClick: handleExportClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
@@ -164,6 +206,9 @@ const WorkflowDetailDesign = () => {
|
||||
|
||||
<WorkflowNodeDrawer {...designerNodeDrawerProps} />
|
||||
</WorkflowDesigner>
|
||||
|
||||
<WorkflowGraphImportModal {...graphImportModalProps} />
|
||||
<WorkflowGraphExportModal {...graphExportModalProps} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,30 +14,15 @@ export const validDomainName = (value: string, { allowWildcard = false }: { allo
|
||||
};
|
||||
|
||||
export const validEmailAddress = (value: string) => {
|
||||
try {
|
||||
z.email().parse(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return z.email().safeParse(value).success;
|
||||
};
|
||||
|
||||
export const validIPv4Address = (value: string) => {
|
||||
try {
|
||||
z.ipv4().parse(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return z.ipv4().safeParse(value).success;
|
||||
};
|
||||
|
||||
export const validIPv6Address = (value: string) => {
|
||||
try {
|
||||
z.ipv6().parse(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return z.ipv6().safeParse(value).success;
|
||||
};
|
||||
|
||||
export const validHttpOrHttpsUrl = (value: string) => {
|
||||
|
||||
Reference in New Issue
Block a user