mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-14 16:39:53 +00:00
feat: release and discard workflow changes
This commit is contained in:
parent
9c4831fa3f
commit
7cf96d7d7e
@ -45,7 +45,7 @@ type WorkflowNode struct {
|
||||
Type WorkflowNodeType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
|
||||
Config map[string]any `json:"config"`
|
||||
Config map[string]any `json:"data"`
|
||||
Inputs []WorkflowNodeIO `json:"inputs"`
|
||||
Outputs []WorkflowNodeIO `json:"outputs"`
|
||||
|
||||
|
@ -42,6 +42,7 @@
|
||||
"workflow.detail.baseinfo.form.description.label": "Description",
|
||||
"workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description",
|
||||
"workflow.detail.orchestration.tab": "Orchestration",
|
||||
"workflow.detail.orchestration.draft.alert": "The orchestration is not released yet.",
|
||||
"workflow.detail.orchestration.action.discard": "Discard changes",
|
||||
"workflow.detail.orchestration.action.discard.confirm": "Are you sure to discard your changes?",
|
||||
"workflow.detail.orchestration.action.release": "Release",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"workflow.detail.baseinfo.form.description.label": "描述",
|
||||
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
|
||||
"workflow.detail.orchestration.tab": "流程编排",
|
||||
"workflow.detail.orchestration.draft.alert": "当前编排有未发布的更改。",
|
||||
"workflow.detail.orchestration.action.discard": "撤销更改",
|
||||
"workflow.detail.orchestration.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
|
||||
"workflow.detail.orchestration.action.release": "发布更改",
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useDeepCompareEffect } from "ahooks";
|
||||
import { 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";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import { isEqual } from "radash";
|
||||
@ -39,15 +39,15 @@ const WorkflowDetail = () => {
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const { id: workflowId } = useParams();
|
||||
const { workflow, initialized, init, save, destroy, setBaseInfo, switchEnable } = useWorkflowStore(
|
||||
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "save", "setBaseInfo", "switchEnable"])
|
||||
const { workflow, initialized, ...workflowState } = useWorkflowStore(
|
||||
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setBaseInfo", "setEnabled", "release", "discard"])
|
||||
);
|
||||
useEffect(() => {
|
||||
// TODO: loading & error
|
||||
init(workflowId!);
|
||||
workflowState.init(workflowId!);
|
||||
|
||||
return () => {
|
||||
destroy();
|
||||
workflowState.destroy();
|
||||
};
|
||||
}, [workflowId]);
|
||||
|
||||
@ -68,7 +68,7 @@ const WorkflowDetail = () => {
|
||||
|
||||
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
||||
try {
|
||||
await setBaseInfo(values.name!, values.description!);
|
||||
await workflowState.setBaseInfo(values.name!, values.description!);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
@ -83,7 +83,7 @@ const WorkflowDetail = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await switchEnable();
|
||||
await workflowState.setEnabled(!workflow.enabled);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
@ -112,8 +112,15 @@ const WorkflowDetail = () => {
|
||||
modalApi.confirm({
|
||||
title: t("workflow.detail.orchestration.action.discard"),
|
||||
content: t("workflow.detail.orchestration.action.discard.confirm"),
|
||||
onOk: () => {
|
||||
alert("TODO");
|
||||
onOk: async () => {
|
||||
try {
|
||||
await workflowState.discard();
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -129,7 +136,7 @@ const WorkflowDetail = () => {
|
||||
content: t("workflow.detail.orchestration.action.release.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await save();
|
||||
await workflowState.release();
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
@ -242,38 +249,45 @@ const WorkflowDetail = () => {
|
||||
<Card loading={!initialized}>
|
||||
<Show when={tabValue === "orchestration"}>
|
||||
<div className="relative">
|
||||
<div className="py-12 lg:pr-36 xl:pr-48">
|
||||
<WorkflowElements />
|
||||
</div>
|
||||
<div className="absolute right-0 top-0 z-[1]">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
<Button.Group>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.detail.orchestration.action.release")}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.detail.orchestration.action.discard"),
|
||||
icon: <UndoOutlinedIcon />,
|
||||
onClick: handleDiscardClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
</Space>
|
||||
<Button.Group>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.detail.orchestration.action.release")}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.detail.orchestration.action.discard"),
|
||||
icon: <UndoOutlinedIcon />,
|
||||
onClick: handleDiscardClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-12 py-8">
|
||||
<WorkflowElements />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -35,7 +35,7 @@ export const get = async (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const save = async (record: Record<string, string | boolean | WorkflowNode>) => {
|
||||
export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
|
||||
if (record.id) {
|
||||
return await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
|
||||
import {
|
||||
type WorkflowModel,
|
||||
type WorkflowNode,
|
||||
type WorkflowNodeConfigForStart,
|
||||
addBranch,
|
||||
addNode,
|
||||
getExecuteMethod,
|
||||
getWorkflowOutputBeforeId,
|
||||
removeBranch,
|
||||
removeNode,
|
||||
@ -16,17 +17,22 @@ import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow"
|
||||
export type WorkflowState = {
|
||||
workflow: WorkflowModel;
|
||||
initialized: boolean;
|
||||
updateNode: (node: WorkflowNode) => void;
|
||||
addNode: (node: WorkflowNode, preId: string) => void;
|
||||
addBranch: (branchId: string) => void;
|
||||
removeNode: (nodeId: string) => void;
|
||||
removeBranch: (branchId: string, index: number) => void;
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||
switchEnable(): void;
|
||||
save(): void;
|
||||
setBaseInfo: (name: string, description: string) => void;
|
||||
|
||||
init(id: string): void;
|
||||
setBaseInfo: (name: string, description: string) => void;
|
||||
setEnabled(enabled: boolean): void;
|
||||
release(): void;
|
||||
discard(): void;
|
||||
destroy(): void;
|
||||
|
||||
addNode: (node: WorkflowNode, preId: string) => void;
|
||||
updateNode: (node: WorkflowNode) => void;
|
||||
removeNode: (nodeId: string) => void;
|
||||
|
||||
addBranch: (branchId: string) => void;
|
||||
removeBranch: (branchId: string, index: number) => void;
|
||||
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||
};
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
@ -42,171 +48,197 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
destroy: () => {
|
||||
set({
|
||||
workflow: {} as WorkflowModel,
|
||||
initialized: false,
|
||||
});
|
||||
},
|
||||
|
||||
setBaseInfo: async (name: string, description: string) => {
|
||||
const data: Record<string, string | boolean | WorkflowNode> = {
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const resp = await saveWorkflow({
|
||||
id: get().workflow.id!,
|
||||
name: name || "",
|
||||
description: description || "",
|
||||
};
|
||||
const resp = await saveWorkflow(data);
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
name,
|
||||
description,
|
||||
id: resp.id,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
switchEnable: async () => {
|
||||
const root = get().workflow.content as WorkflowNode;
|
||||
const executeMethod = getExecuteMethod(root);
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
content: root,
|
||||
enabled: !get().workflow.enabled,
|
||||
trigger: executeMethod.trigger,
|
||||
triggerCron: executeMethod.triggerCron,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
id: resp.id,
|
||||
content: resp.content,
|
||||
enabled: resp.enabled,
|
||||
trigger: resp.trigger,
|
||||
triggerCron: resp.triggerCron,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.name = resp.name;
|
||||
draft.description = resp.description;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
save: async () => {
|
||||
const root = get().workflow.draft as WorkflowNode;
|
||||
const executeMethod = getExecuteMethod(root);
|
||||
setEnabled: async (enabled: boolean) => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
id: get().workflow.id!,
|
||||
enabled: enabled,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.enabled = resp.enabled;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
release: async () => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = get().workflow.draft!;
|
||||
const startConfig = root.config as WorkflowNodeConfigForStart;
|
||||
const resp = await saveWorkflow({
|
||||
id: get().workflow.id!,
|
||||
trigger: startConfig.trigger,
|
||||
triggerCron: startConfig.triggerCron,
|
||||
content: root,
|
||||
hasDraft: false,
|
||||
trigger: executeMethod.trigger,
|
||||
triggerCron: executeMethod.triggerCron,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
id: resp.id,
|
||||
content: resp.content,
|
||||
hasDraft: false,
|
||||
trigger: resp.trigger,
|
||||
triggerCron: resp.triggerCron,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.trigger = resp.trigger;
|
||||
draft.triggerCron = resp.triggerCron;
|
||||
draft.content = resp.content;
|
||||
draft.draft = resp.draft;
|
||||
draft.hasDraft = resp.hasDraft;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
updateNode: async (node: WorkflowNode) => {
|
||||
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
|
||||
discard: async () => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = get().workflow.content!;
|
||||
const startConfig = root.config as WorkflowNodeConfigForStart;
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
id: get().workflow.id!,
|
||||
draft: root,
|
||||
hasDraft: false,
|
||||
trigger: startConfig.trigger,
|
||||
triggerCron: startConfig.triggerCron,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.trigger = resp.trigger;
|
||||
draft.triggerCron = resp.triggerCron;
|
||||
draft.content = resp.content;
|
||||
draft.draft = resp.draft;
|
||||
draft.hasDraft = resp.hasDraft;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addNode: async (node: WorkflowNode, preId: string) => {
|
||||
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = addNode(get().workflow.draft!, preId, node);
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
id: get().workflow.id!,
|
||||
draft: root,
|
||||
hasDraft: true,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.draft = resp.draft;
|
||||
draft.hasDraft = resp.hasDraft;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addBranch: async (branchId: string) => {
|
||||
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
|
||||
updateNode: async (node: WorkflowNode) => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = updateNode(get().workflow.draft!, node);
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
id: get().workflow.id!,
|
||||
draft: root,
|
||||
hasDraft: true,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeBranch: async (branchId: string, index: number) => {
|
||||
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.draft = resp.draft;
|
||||
draft.hasDraft = resp.hasDraft;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeNode: async (nodeId: string) => {
|
||||
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = removeNode(get().workflow.draft!, nodeId);
|
||||
const resp = await saveWorkflow({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
id: get().workflow.id!,
|
||||
draft: root,
|
||||
hasDraft: true,
|
||||
});
|
||||
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
workflow: produce(state.workflow, (draft) => {
|
||||
draft.draft = resp.draft;
|
||||
draft.hasDraft = resp.hasDraft;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addBranch: async (branchId: string) => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = addBranch(get().workflow.draft!, branchId);
|
||||
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;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeBranch: async (branchId: string, index: number) => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = removeBranch(get().workflow.draft!, branchId, index);
|
||||
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;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
@ -214,11 +246,4 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||
},
|
||||
|
||||
destroy: () => {
|
||||
set({
|
||||
workflow: {} as WorkflowModel,
|
||||
initialized: false,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
Loading…
x
Reference in New Issue
Block a user