feat(ui): release & run workflow

This commit is contained in:
Fu Diwei 2025-01-01 16:53:52 +08:00
parent 5c1854948c
commit 6075cc5c95
17 changed files with 197 additions and 161 deletions

View File

@ -199,7 +199,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string]
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")), ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"), PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"), PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")), OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", string(providerLocal.OUTPUT_FORMAT_PEM))),
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"), OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"), OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"), PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
@ -259,7 +259,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string]
SshKeyPassphrase: access.KeyPassphrase, SshKeyPassphrase: access.KeyPassphrase,
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"), PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"), PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")), OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))),
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"), OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"), OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"), PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),

View File

@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { Button, Form, Input, message, notification, Skeleton } from "antd"; import { Button, Form, Input, message, notification, Skeleton } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { z } from "zod";
import Show from "@/components/Show"; import Show from "@/components/Show";
import { useAntdForm } from "@/hooks"; import { useAntdForm } from "@/hooks";

View File

@ -353,17 +353,14 @@ const FormFieldDomainsModalForm = ({
setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) }); setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
}, [data]); }, [data]);
const handleFinish = useCallback( const handleFormFinish = (values: z.infer<typeof formSchema>) => {
(values: z.infer<typeof formSchema>) => { onFinish?.(
onFinish?.( values.domains
values.domains .map((e) => e.trim())
.map((e) => e.trim()) .filter((e) => !!e)
.filter((e) => !!e) .join(MULTIPLE_INPUT_DELIMITER)
.join(MULTIPLE_INPUT_DELIMITER) );
); };
},
[onFinish]
);
return ( return (
<ModalForm <ModalForm
@ -375,7 +372,7 @@ const FormFieldDomainsModalForm = ({
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"
width={480} width={480}
onFinish={handleFinish} onFinish={handleFormFinish}
> >
<Form.Item name="domains" rules={[formRule]}> <Form.Item name="domains" rules={[formRule]}>
<MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} /> <MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} />
@ -400,17 +397,14 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) }); setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
}, [data]); }, [data]);
const handleFinish = useCallback( const handleFormFinish = (values: z.infer<typeof formSchema>) => {
(values: z.infer<typeof formSchema>) => { onFinish?.(
onFinish?.( values.nameservers
values.nameservers .map((e) => e.trim())
.map((e) => e.trim()) .filter((e) => !!e)
.filter((e) => !!e) .join(MULTIPLE_INPUT_DELIMITER)
.join(MULTIPLE_INPUT_DELIMITER) );
); };
},
[onFinish]
);
return ( return (
<ModalForm <ModalForm
@ -422,7 +416,7 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"
width={480} width={480}
onFinish={handleFinish} onFinish={handleFormFinish}
> >
<Form.Item name="nameservers" rules={[formRule]}> <Form.Item name="nameservers" rules={[formRule]}>
<MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} /> <MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} />

View File

@ -6,9 +6,9 @@ import { z } from "zod";
import Show from "@/components/Show"; import Show from "@/components/Show";
const FORMAT_PEM = "pem" as const; const FORMAT_PEM = "PEM" as const;
const FORMAT_PFX = "pfx" as const; const FORMAT_PFX = "PFX" as const;
const FORMAT_JKS = "jks" as const; const FORMAT_JKS = "JKS" as const;
const SHELLENV_SH = "sh" as const; const SHELLENV_SH = "sh" as const;
const SHELLENV_CMD = "cmd" as const; const SHELLENV_CMD = "cmd" as const;

View File

@ -6,9 +6,9 @@ import { z } from "zod";
import Show from "@/components/Show"; import Show from "@/components/Show";
const FORMAT_PEM = "pem" as const; const FORMAT_PEM = "PEM" as const;
const FORMAT_PFX = "pfx" as const; const FORMAT_PFX = "PFX" as const;
const FORMAT_JKS = "jks" as const; const FORMAT_JKS = "JKS" as const;
const DeployNodeFormSSHFields = () => { const DeployNodeFormSSHFields = () => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -32,28 +32,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}> <Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
<Show when={!!data}> <Show when={!!data}>
<Show when={data!.succeed}> <Show when={data!.succeed}>
<Alert <Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
showIcon
type="success"
message={
<>
<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>
</>
}
/>
</Show> </Show>
<Show when={!!data!.error}> <Show when={!!data!.error}>
<Alert <Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
showIcon
type="error"
message={
<>
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
</>
}
description={data!.error}
/>
</Show> </Show>
<div className="mt-4 p-4 bg-black text-stone-200 rounded-md"> <div className="mt-4 p-4 bg-black text-stone-200 rounded-md">

View File

@ -54,12 +54,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
</Space> </Space>
); );
} else { } else {
<Tooltip title={record.error}> return (
<Space> <Space>
<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} /> <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text> <Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
</Space> </Space>
</Tooltip>; );
} }
}, },
}, },

View File

@ -362,20 +362,20 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod
return output; return output;
}; };
export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => { export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => {
let current = node; let current = node as typeof node | undefined;
while (current) { while (current) {
if (!isWorkflowBranchNode(current) && !current.validated) { if (!isWorkflowBranchNode(current) && !current.validated) {
return false; return false;
} }
if (isWorkflowBranchNode(current)) { if (isWorkflowBranchNode(current)) {
for (const branch of current.branches) { for (const branch of current.branches) {
if (!allNodesValidated(branch)) { if (!isAllNodesValidated(branch)) {
return false; return false;
} }
} }
} }
current = current.next as WorkflowNode; current = current.next;
} }
return true; return true;
}; };

View File

@ -7,11 +7,14 @@
"workflow.action.edit": "Edit workflow", "workflow.action.edit": "Edit workflow",
"workflow.action.delete": "Delete workflow", "workflow.action.delete": "Delete workflow",
"workflow.action.delete.confirm": "Are you sure to delete this workflow?", "workflow.action.delete.confirm": "Are you sure to delete this workflow?",
"workflow.action.discard": "Discard", "workflow.action.discard": "Discard changes",
"workflow.action.discard.confirm": "Are you sure to discard your changes?", "workflow.action.discard.confirm": "Are you sure to discard your changes?",
"workflow.action.release": "Release", "workflow.action.release": "Release",
"workflow.action.release.confirm": "Are you sure to release your changes?", "workflow.action.release.confirm": "Are you sure to release your changes?",
"workflow.action.execute": "Run", "workflow.action.release.failed.uncompleted": "Please complete the orchestration first",
"workflow.action.run": "Run",
"workflow.action.run.confirm": "There are unreleased changes, are you sure to run this workflow based on the latest released version?",
"workflow.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first",
"workflow.props.name": "Name", "workflow.props.name": "Name",
"workflow.props.description": "Description", "workflow.props.description": "Description",
@ -35,13 +38,6 @@
"workflow.detail.baseinfo.form.description.placeholder": "Please enter description", "workflow.detail.baseinfo.form.description.placeholder": "Please enter description",
"workflow.common.certificate.label": "Certificate", "workflow.common.certificate.label": "Certificate",
"workflow.detail.action.save": "Save updates",
"workflow.detail.action.save.failed": "Save failed",
"workflow.detail.action.save.failed.uncompleted": "Please complete the orchestration and publish the changes first",
"workflow.detail.action.run": "Run",
"workflow.detail.action.run.failed": "Run failed",
"workflow.detail.action.run.success": "Run success",
"workflow.detail.action.running": "Running",
"workflow.node.setting.label": "Setting Node", "workflow.node.setting.label": "Setting Node",
"workflow.node.delete.label": "Delete Node", "workflow.node.delete.label": "Delete Node",
"workflow.node.addBranch.label": "Add Branch", "workflow.node.addBranch.label": "Add Branch",

View File

@ -11,8 +11,10 @@
"workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?", "workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
"workflow.action.release": "发布更改", "workflow.action.release": "发布更改",
"workflow.action.release.confirm": "确定要发布更改吗?", "workflow.action.release.confirm": "确定要发布更改吗?",
"workflow.action.execute": "执行", "workflow.action.release.failed.uncompleted": "请先完成流程编排",
"workflow.action.execute.confirm": "确定要立即执行此工作流吗?", "workflow.action.run": "执行",
"workflow.action.run.confirm": "存在未发布的更改,确定要按最近一次发布的版本来执行此工作流吗?",
"workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改",
"workflow.props.name": "名称", "workflow.props.name": "名称",
"workflow.props.description": "描述", "workflow.props.description": "描述",
@ -36,13 +38,6 @@
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述", "workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
"workflow.common.certificate.label": "证书", "workflow.common.certificate.label": "证书",
"workflow.detail.action.save": "保存变更",
"workflow.detail.action.save.failed": "保存失败",
"workflow.detail.action.save.failed.uncompleted": "请先完成流程编排并发布更改",
"workflow.detail.action.run": "立即执行",
"workflow.detail.action.run.failed": "执行失败",
"workflow.detail.action.run.success": "执行成功",
"workflow.detail.action.running": "正在执行",
"workflow.node.setting.label": "设置节点", "workflow.node.setting.label": "设置节点",
"workflow.node.delete.label": "删除节点", "workflow.node.delete.label": "删除节点",
"workflow.node.addBranch.label": "添加分支", "workflow.node.addBranch.label": "添加分支",

View File

@ -28,7 +28,7 @@ const Login = () => {
onSubmit: async (values) => { onSubmit: async (values) => {
try { try {
await getPocketBase().admins.authWithPassword(values.username, values.password); await getPocketBase().admins.authWithPassword(values.username, values.password);
navigage("/"); await navigage("/");
} catch (err) { } catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
} }

View File

@ -1,14 +1,18 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Tabs, Typography } from "antd"; import { useDeepCompareEffect } from "ahooks";
import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Space, Tabs, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { import {
CaretRightOutlined as CaretRightOutlinedIcon, CaretRightOutlined as CaretRightOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
EllipsisOutlined as EllipsisOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon,
UndoOutlined as UndoOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { ClientResponseError } from "pocketbase";
import { isEqual } from "radash";
import { z } from "zod"; import { z } from "zod";
import Show from "@/components/Show"; import Show from "@/components/Show";
@ -18,9 +22,10 @@ import NodeRender from "@/components/workflow/NodeRender";
import WorkflowRuns from "@/components/workflow/run/WorkflowRuns"; import WorkflowRuns from "@/components/workflow/run/WorkflowRuns";
import WorkflowProvider from "@/components/workflow/WorkflowProvider"; import WorkflowProvider from "@/components/workflow/WorkflowProvider";
import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { allNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow"; import { isAllNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow"; import { useWorkflowStore } from "@/stores/workflow";
import { remove as removeWorkflow } from "@/repository/workflow"; import { remove as removeWorkflow } from "@/repository/workflow";
import { run as runWorkflow } from "@/api/workflow";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
const WorkflowDetail = () => { const WorkflowDetail = () => {
@ -33,16 +38,17 @@ const WorkflowDetail = () => {
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { id: workflowId } = useParams(); const { id: workflowId } = useParams();
const { workflow, init, setBaseInfo, switchEnable } = useWorkflowStore(useZustandShallowSelector(["workflow", "init", "setBaseInfo", "switchEnable"])); const { workflow, init, save, setBaseInfo, switchEnable } = useWorkflowStore(
useZustandShallowSelector(["workflow", "init", "save", "setBaseInfo", "switchEnable"])
);
useEffect(() => { useEffect(() => {
// TODO: loading
init(workflowId); init(workflowId);
}, [workflowId, init]); }, [workflowId, init]);
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
// const [running, setRunning] = useState(false); const workflowNodes = useMemo(() => {
const elements = useMemo(() => {
let current = workflow.draft as WorkflowNode; let current = workflow.draft as WorkflowNode;
const elements: JSX.Element[] = []; const elements: JSX.Element[] = [];
@ -58,6 +64,16 @@ const WorkflowDetail = () => {
return elements; return elements;
}, [workflow]); }, [workflow]);
const [workflowRunning, setWorkflowRunning] = useState(false);
const [allowDiscard, setAllowDiscard] = useState(false);
const [allowRelease, setAllowRelease] = useState(false);
useDeepCompareEffect(() => {
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
setAllowDiscard(hasChanges && !workflowRunning);
setAllowRelease(hasChanges && !workflowRunning);
}, [workflow, workflowRunning]);
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => { const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
try { try {
await setBaseInfo(values.name!, values.description!); await setBaseInfo(values.name!, values.description!);
@ -69,10 +85,11 @@ const WorkflowDetail = () => {
}; };
const handleEnableChange = () => { const handleEnableChange = () => {
if (!workflow.enabled && !allNodesValidated(workflow.content!)) { if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
messageApi.warning(t("workflow.detail.action.save.failed.uncompleted")); messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
return; return;
} }
switchEnable(); switchEnable();
}; };
@ -84,7 +101,7 @@ const WorkflowDetail = () => {
try { try {
const resp: boolean = await removeWorkflow(workflow); const resp: boolean = await removeWorkflow(workflow);
if (resp) { if (resp) {
navigate("/workflows"); navigate("/workflows", { replace: true });
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -94,29 +111,59 @@ const WorkflowDetail = () => {
}); });
}; };
// TODO: 发布更改 撤销更改 立即执行 const handleDiscardClick = () => {
// const handleWorkflowSaveClick = () => { alert("TODO");
// if (!allNodesValidated(workflow.draft as WorkflowNode)) { };
// messageApi.warning(t("workflow.detail.action.save.failed.uncompleted"));
// return;
// }
// save();
// };
// const handleRunClick = async () => { const handleReleaseClick = () => {
// if (running) return; if (!isAllNodesValidated(workflow.draft!)) {
messageApi.warning(t("workflow.action.release.failed.uncompleted"));
return;
}
// setRunning(true); save();
// try {
// await runWorkflow(workflow.id as string); messageApi.success(t("common.text.operation_succeeded"));
// messageApi.success(t("workflow.detail.action.run.success")); };
// } catch (err) {
// console.error(err); const handleRunClick = () => {
// messageApi.warning(t("workflow.detail.action.run.failed")); if (!workflow.enabled) {
// } finally { alert("TODO: 暂时只支持执行已启用的工作流");
// setRunning(false); return;
// } }
// };
const { promise, resolve, reject } = Promise.withResolvers();
if (workflow.hasDraft) {
modalApi.confirm({
title: t("workflow.action.run"),
content: t("workflow.action.run.confirm"),
onOk: () => resolve(void 0),
onCancel: () => reject(),
});
} else {
resolve(void 0);
}
// TODO: 异步执行
promise.then(async () => {
setWorkflowRunning(true);
try {
await runWorkflow(workflowId!);
messageApi.warning(t("common.text.operation_succeeded"));
} catch (err) {
if (err instanceof ClientResponseError && err.isAbort) {
return;
}
console.error(err);
messageApi.warning(t("common.text.operation_failed"));
} finally {
setWorkflowRunning(false);
}
});
};
return ( return (
<div> <div>
@ -175,17 +222,37 @@ const WorkflowDetail = () => {
<Show when={tabValue === "orchestration"}> <Show when={tabValue === "orchestration"}>
<div className="relative"> <div className="relative">
<div className="flex flex-col items-center py-12 pr-48"> <div className="flex flex-col items-center py-12 pr-48">
<WorkflowProvider>{elements}</WorkflowProvider> <WorkflowProvider>{workflowNodes}</WorkflowProvider>
</div> </div>
<div className="absolute top-0 right-0 z-[1]"> <div className="absolute top-0 right-0 z-[1]">
<Button.Group> <Space>
<Button onClick={() => alert("TODO")}>{t("workflow.action.discard")}</Button> <Button icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
<Button onClick={() => alert("TODO")}>{t("workflow.action.release")}</Button> {t("workflow.action.run")}
<Button type="primary" onClick={() => alert("TODO")}>
<CaretRightOutlinedIcon />
{t("workflow.action.execute")}
</Button> </Button>
</Button.Group>
<Button.Group>
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
{t("workflow.action.release")}
</Button>
<Dropdown
menu={{
items: [
{
key: "discard",
disabled: !allowDiscard,
label: t("workflow.action.discard"),
icon: <UndoOutlinedIcon />,
onClick: handleDiscardClick,
},
],
}}
trigger={["click"]}
>
<Button color="primary" icon={<EllipsisOutlinedIcon />} variant="outlined" />
</Dropdown>
</Button.Group>
</Space>
</div> </div>
</div> </div>
</Show> </Show>
@ -237,7 +304,7 @@ const WorkflowBaseInfoModalForm = ({
}, },
}); });
const handleFinish = async () => { const handleFormFinish = async () => {
return formApi.submit(); return formApi.submit();
}; };
@ -252,7 +319,7 @@ const WorkflowBaseInfoModalForm = ({
trigger={trigger} trigger={trigger}
width={480} width={480}
{...formProps} {...formProps}
onFinish={handleFinish} onFinish={handleFormFinish}
> >
<Form.Item name="name" label={t("workflow.detail.baseinfo.form.name.label")} rules={[formRule]}> <Form.Item name="name" label={t("workflow.detail.baseinfo.form.name.label")} rules={[formRule]}>
<Input placeholder={t("workflow.detail.baseinfo.form.name.placeholder")} /> <Input placeholder={t("workflow.detail.baseinfo.form.name.placeholder")} />
@ -264,4 +331,5 @@ const WorkflowBaseInfoModalForm = ({
</ModalForm> </ModalForm>
); );
}; };
export default WorkflowDetail; export default WorkflowDetail;

View File

@ -25,7 +25,7 @@ import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon,
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { allNodesValidated, type WorkflowModel } from "@/domain/workflow"; import { isAllNodesValidated, type WorkflowModel } from "@/domain/workflow";
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
@ -241,8 +241,8 @@ const WorkflowList = () => {
const handleEnabledChange = async (workflow: WorkflowModel) => { const handleEnabledChange = async (workflow: WorkflowModel) => {
try { try {
if (!workflow.enabled && !allNodesValidated(workflow.content!)) { if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
messageApi.warning(t("workflow.detail.action.save.failed.uncompleted")); messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
return; return;
} }

View File

@ -23,6 +23,19 @@ export const useAccessStore = create<AccessState>((set) => {
loading: false, loading: false,
loadedAtOnce: false, loadedAtOnce: false,
fetchAccesses: async () => {
fetcher ??= listAccess();
try {
set({ loading: true });
const accesses = await fetcher;
set({ accesses: accesses ?? [], loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
createAccess: async (access) => { createAccess: async (access) => {
const record = await saveAccess(access); const record = await saveAccess(access);
set( set(
@ -58,18 +71,5 @@ export const useAccessStore = create<AccessState>((set) => {
return access as AccessModel; return access as AccessModel;
}, },
fetchAccesses: async () => {
fetcher ??= listAccess();
try {
set({ loading: true });
const accesses = await fetcher;
set({ accesses: accesses ?? [], loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
}; };
}); });

View File

@ -24,6 +24,19 @@ export const useContactStore = create<ContactState>((set, get) => {
loading: false, loading: false,
loadedAtOnce: false, loadedAtOnce: false,
fetchEmails: async () => {
fetcher ??= getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
try {
set({ loading: true });
settings = await fetcher;
set({ emails: settings.content.emails?.sort() ?? [], loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
setEmails: async (emails) => { setEmails: async (emails) => {
settings ??= await getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS); settings ??= await getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
settings = await saveSettings<EmailsSettingsContent>({ settings = await saveSettings<EmailsSettingsContent>({
@ -58,18 +71,5 @@ export const useContactStore = create<ContactState>((set, get) => {
}); });
get().setEmails(emails); get().setEmails(emails);
}, },
fetchEmails: async () => {
fetcher ??= getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
try {
set({ loading: true });
settings = await fetcher;
set({ emails: settings.content.emails?.sort() ?? [], loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
}; };
}); });

View File

@ -23,6 +23,19 @@ export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
loading: false, loading: false,
loadedAtOnce: false, loadedAtOnce: false,
fetchChannels: async () => {
fetcher ??= getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
try {
set({ loading: true });
settings = await fetcher;
set({ channels: settings.content ?? {}, loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
setChannel: async (channel, config) => { setChannel: async (channel, config) => {
settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS); settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
return get().setChannels( return get().setChannels(
@ -47,18 +60,5 @@ export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
}) })
); );
}, },
fetchChannels: async () => {
fetcher ??= getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
try {
set({ loading: true });
settings = await fetcher;
set({ channels: settings.content ?? {}, loadedAtOnce: true });
} finally {
fetcher = null;
set({ loading: false });
}
},
}; };
}); });

View File

@ -5,9 +5,9 @@
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": [ "lib": [
"ES2020",
"DOM", "DOM",
"DOM.Iterable" "DOM.Iterable",
"ESNext",
], ],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,