feat(ui): WorkflowNew page

This commit is contained in:
Fu Diwei 2025-01-02 20:24:16 +08:00
parent b6dd2248c8
commit c6a8f923e4
21 changed files with 415 additions and 225 deletions

View File

@ -37,8 +37,8 @@ type WorkflowNode struct {
Name string `json:"name"` Name string `json:"name"`
Next *WorkflowNode `json:"next"` Next *WorkflowNode `json:"next"`
Config map[string]any `json:"config"` Config map[string]any `json:"config"`
Input []WorkflowNodeIo `json:"input"` Input []WorkflowNodeIO `json:"input"`
Output []WorkflowNodeIo `json:"output"` Output []WorkflowNodeIO `json:"output"`
Validated bool `json:"validated"` Validated bool `json:"validated"`
Type string `json:"type"` Type string `json:"type"`
@ -76,16 +76,16 @@ func (n *WorkflowNode) GetConfigInt64(key string) int64 {
return 0 return 0
} }
type WorkflowNodeIo struct { type WorkflowNodeIO struct {
Label string `json:"label"` Label string `json:"label"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Required bool `json:"required"` Required bool `json:"required"`
Value any `json:"value"` Value any `json:"value"`
ValueSelector WorkflowNodeIoValueSelector `json:"valueSelector"` ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
} }
type WorkflowNodeIoValueSelector struct { type WorkflowNodeIOValueSelector struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }

View File

@ -7,6 +7,6 @@ type WorkflowOutput struct {
Workflow string `json:"workflow"` Workflow string `json:"workflow"`
NodeId string `json:"nodeId"` NodeId string `json:"nodeId"`
Node *WorkflowNode `json:"node"` Node *WorkflowNode `json:"node"`
Output []WorkflowNodeIo `json:"output"` Output []WorkflowNodeIO `json:"output"`
Succeed bool `json:"succeed"` Succeed bool `json:"succeed"`
} }

View File

@ -35,7 +35,7 @@ func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*dom
return nil, errors.New("failed to unmarshal node") return nil, errors.New("failed to unmarshal node")
} }
output := make([]domain.WorkflowNodeIo, 0) output := make([]domain.WorkflowNodeIO, 0)
if err := record.UnmarshalJSONField("output", &output); err != nil { if err := record.UnmarshalJSONField("output", &output); err != nil {
return nil, errors.New("failed to unmarshal output") return nil, errors.New("failed to unmarshal output")
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BookOutlined as BookOutlinedIcon } from "@ant-design/icons"; import { ReadOutlined as ReadOutlinedIcon } from "@ant-design/icons";
import { Divider, Space, Typography } from "antd"; import { Divider, Space, Typography } from "antd";
import { version } from "@/domain/version"; import { version } from "@/domain/version";
@ -16,7 +16,7 @@ const Version = ({ className, style }: VersionProps) => {
<Space className={className} style={style} size={4}> <Space className={className} style={style} size={4}>
<Typography.Link type="secondary" href="https://docs.certimate.me" target="_blank"> <Typography.Link type="secondary" href="https://docs.certimate.me" target="_blank">
<div className="flex items-center justify-center space-x-1"> <div className="flex items-center justify-center space-x-1">
<BookOutlinedIcon /> <ReadOutlinedIcon />
<span>{t("common.menu.document")}</span> <span>{t("common.menu.document")}</span>
</div> </div>
</Typography.Link> </Typography.Link>

View File

@ -1,7 +1,7 @@
import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons"; import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import { Dropdown } from "antd"; import { Dropdown } from "antd";
import { newWorkflowNode, workflowNodeDropdownList, type WorkflowNodeType } from "@/domain/workflow"; import { type WorkflowNodeType, newNode, workflowNodeDropdownList } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks"; import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow"; import { useWorkflowStore } from "@/stores/workflow";
@ -12,7 +12,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"])); const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"]));
const handleTypeSelected = (type: WorkflowNodeType, provider?: string) => { const handleTypeSelected = (type: WorkflowNodeType, provider?: string) => {
const node = newWorkflowNode(type, { const node = newNode(type, {
providerType: provider, providerType: provider,
}); });

View File

@ -1,11 +1,18 @@
import { CloudUpload, GitFork, Megaphone, NotebookPen } from "lucide-react"; import {
CloudUploadOutlined as CloudUploadOutlinedIcon,
SendOutlined as SendOutlinedIcon,
SisternodeOutlined as SisternodeOutlinedIcon,
SolutionOutlined as SolutionOutlinedIcon,
} from "@ant-design/icons";
import { Avatar } from "antd";
import { type WorkflowNodeDropdwonItemIcon, WorkflowNodeDropdwonItemIconType } from "@/domain/workflow"; import { type WorkflowNodeDropdwonItemIcon, WorkflowNodeDropdwonItemIconType } from "@/domain/workflow";
const icons = new Map([ const icons = new Map([
["NotebookPen", <NotebookPen size={16} />], ["ApplyNodeIcon", <SolutionOutlinedIcon />],
["CloudUpload", <CloudUpload size={16} />], ["DeployNodeIcon", <CloudUploadOutlinedIcon />],
["GitFork", <GitFork size={16} />], ["BranchNodeIcon", <SisternodeOutlinedIcon />],
["Megaphone", <Megaphone size={16} />], ["NotifyNodeIcon", <SendOutlinedIcon />],
]); ]);
const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => { const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => {
@ -13,7 +20,7 @@ const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => {
if (type === WorkflowNodeDropdwonItemIconType.Icon) { if (type === WorkflowNodeDropdwonItemIconType.Icon) {
return icons.get(name); return icons.get(name);
} else { } else {
return <img src={name} className="inline-block size-4" />; return <Avatar src={name} size="small" />;
} }
}; };

View File

@ -8,7 +8,7 @@ export interface CertificateModel extends BaseModel {
certUrl: string; certUrl: string;
certStableUrl: string; certStableUrl: string;
output: string; output: string;
expireAt: string; expireAt: ISO8601String;
workflow: string; workflow: string;
nodeId: string; nodeId: string;
expand: { expand: {

View File

@ -1,16 +1,10 @@
import dayjs from "dayjs";
import { produce } from "immer"; import { produce } from "immer";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { deployProvidersMap } from "./provider"; import { deployProvidersMap } from "./provider";
export type WorkflowOutput = {
time: string;
title: string;
content: string;
error: string;
};
export interface WorkflowModel extends BaseModel { export interface WorkflowModel extends BaseModel {
name: string; name: string;
description?: string; description?: string;
@ -22,6 +16,7 @@ export interface WorkflowModel extends BaseModel {
hasDraft?: boolean; hasDraft?: boolean;
} }
// #region Node
export enum WorkflowNodeType { export enum WorkflowNodeType {
Start = "start", Start = "start",
End = "end", End = "end",
@ -33,7 +28,7 @@ export enum WorkflowNodeType {
Custom = "custom", Custom = "custom",
} }
export const workflowNodeTypeDefaultName: 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")],
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
@ -44,21 +39,7 @@ export const workflowNodeTypeDefaultName: Map<WorkflowNodeType, string> = new Ma
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")], [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
]); ]);
export type WorkflowNodeIo = { const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
name: string;
type: string;
required: boolean;
label: string;
value?: string;
valueSelector?: WorkflowNodeIoValueSelector;
};
export type WorkflowNodeIoValueSelector = {
id: string;
name: string;
};
export const workflowNodeTypeDefaultInput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([
[WorkflowNodeType.Apply, []], [WorkflowNodeType.Apply, []],
[ [
WorkflowNodeType.Deploy, WorkflowNodeType.Deploy,
@ -74,7 +55,7 @@ export const workflowNodeTypeDefaultInput: Map<WorkflowNodeType, WorkflowNodeIo[
[WorkflowNodeType.Notify, []], [WorkflowNodeType.Notify, []],
]); ]);
export const workflowNodeTypeDefaultOutput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
[ [
WorkflowNodeType.Apply, WorkflowNodeType.Apply,
[ [
@ -90,88 +71,122 @@ export const workflowNodeTypeDefaultOutput: Map<WorkflowNodeType, WorkflowNodeIo
[WorkflowNodeType.Notify, []], [WorkflowNodeType.Notify, []],
]); ]);
export type WorkflowNodeConfig = Record<string, unknown>;
export type WorkflowNode = { export type WorkflowNode = {
id: string; id: string;
name: string; name: string;
type: WorkflowNodeType; type: WorkflowNodeType;
validated?: boolean;
input?: WorkflowNodeIo[]; config?: Record<string, unknown>;
config?: WorkflowNodeConfig; input?: WorkflowNodeIO[];
output?: WorkflowNodeIo[]; output?: WorkflowNodeIO[];
next?: WorkflowNode | WorkflowBranchNode;
branches?: WorkflowNode[];
validated?: boolean;
};
/**
* @deprecated
*/
export type WorkflowBranchNode = {
id: string;
name: string;
type: WorkflowNodeType.Branch;
branches: WorkflowNode[];
next?: WorkflowNode | WorkflowBranchNode; next?: WorkflowNode | WorkflowBranchNode;
}; };
type NewWorkflowNodeOptions = { export type WorkflowNodeIO = {
name: string;
type: string;
required: boolean;
label: string;
value?: string;
valueSelector?: WorkflowNodeIOValueSelector;
};
export type WorkflowNodeIOValueSelector = {
id: string;
name: string;
};
// #endregion
type InitWorkflowOptions = {
template?: "standard";
};
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode;
root.config = { executionMethod: "manual" };
if (options.template === "standard") {
let temp = root;
temp.next = newNode(WorkflowNodeType.Apply, {});
temp = temp.next;
temp.next = newNode(WorkflowNodeType.Deploy, {});
temp = temp.next;
temp.next = newNode(WorkflowNodeType.Notify, {});
}
return {
id: null!,
name: `MyWorkflow-${dayjs().format("YYYYMMDDHHmmss")}`,
type: root.config!.executionMethod as string,
crontab: root.config!.crontab as string,
enabled: false,
draft: root,
hasDraft: true,
created: new Date().toISOString(),
updated: new Date().toISOString(),
};
};
type NewNodeOptions = {
branchIndex?: number; branchIndex?: number;
providerType?: string; providerType?: string;
}; };
export const initWorkflow = (): WorkflowModel => { export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions): WorkflowNode | WorkflowBranchNode => {
// 开始节点 const nodeTypeName = workflowNodeTypeDefaultNames.get(nodeType) || "";
const rs = newWorkflowNode(WorkflowNodeType.Start, {}); const nodeName = options.branchIndex != null ? `${nodeTypeName} ${options.branchIndex + 1}` : nodeTypeName;
let root = rs;
// 申请节点 const node: WorkflowNode | WorkflowBranchNode = {
root.next = newWorkflowNode(WorkflowNodeType.Apply, {}); id: nanoid(),
root = root.next; name: nodeName,
type: nodeType,
// 部署节点
root.next = newWorkflowNode(WorkflowNodeType.Deploy, {});
root = root.next;
// 通知节点
root.next = newWorkflowNode(WorkflowNodeType.Notify, {});
return {
id: "",
name: i18n.t("workflow.props.name.default"),
type: "auto",
crontab: "0 0 * * *",
enabled: false,
draft: rs,
created: new Date().toUTCString(),
updated: new Date().toUTCString(),
};
};
export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => {
const id = nanoid();
const typeName = workflowNodeTypeDefaultName.get(type) || "";
const name = options.branchIndex !== undefined ? `${typeName} ${options.branchIndex + 1}` : typeName;
let rs: WorkflowNode | WorkflowBranchNode = {
id,
name,
type,
}; };
if (type === WorkflowNodeType.Apply || type === WorkflowNodeType.Deploy) { switch (nodeType) {
rs = { case WorkflowNodeType.Apply:
...rs, case WorkflowNodeType.Deploy:
config: { {
providerType: options.providerType, node.config = {
}, providerType: options.providerType,
input: workflowNodeTypeDefaultInput.get(type), };
output: workflowNodeTypeDefaultOutput.get(type), node.input = workflowNodeTypeDefaultInputs.get(nodeType);
}; node.output = workflowNodeTypeDefaultOutputs.get(nodeType);
}
break;
case WorkflowNodeType.Condition:
{
node.validated = true;
}
break;
case WorkflowNodeType.Branch:
{
node.branches = [newNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newNode(WorkflowNodeType.Condition, { branchIndex: 1 })];
}
break;
} }
if (type == WorkflowNodeType.Condition) { return node;
rs.validated = true;
}
if (type === WorkflowNodeType.Branch) {
rs = {
...rs,
branches: [newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 1 })],
};
}
return rs;
}; };
export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => { export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => {
@ -226,7 +241,7 @@ export const addBranch = (node: WorkflowNode | WorkflowBranchNode, branchNodeId:
return draft; return draft;
} }
current.branches.push( current.branches.push(
newWorkflowNode(WorkflowNodeType.Condition, { newNode(WorkflowNodeType.Condition, {
branchIndex: current.branches.length, branchIndex: current.branches.length,
}) })
); );
@ -340,21 +355,24 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod
return output; return output;
}; };
export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => { export const isAllNodesValidated = (node: WorkflowNode): boolean => {
let current = node as typeof node | undefined; let current = node as typeof node | undefined;
while (current) { while (current) {
if (!isWorkflowBranchNode(current) && !current.validated) { if (current.type === WorkflowNodeType.Branch) {
return false; for (const branch of current.branches!) {
}
if (isWorkflowBranchNode(current)) {
for (const branch of current.branches) {
if (!isAllNodesValidated(branch)) { if (!isAllNodesValidated(branch)) {
return false; return false;
} }
} }
} else {
if (!current.validated) {
return false;
}
} }
current = current.next; current = current.next;
} }
return true; return true;
}; };
@ -372,14 +390,9 @@ export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: s
} }
}; };
export type WorkflowBranchNode = { /**
id: string; * @deprecated
name: string; */
type: WorkflowNodeType;
branches: WorkflowNode[];
next?: WorkflowNode | WorkflowBranchNode;
};
type WorkflowNodeDropdwonItem = { type WorkflowNodeDropdwonItem = {
type: WorkflowNodeType; type: WorkflowNodeType;
providerType?: string; providerType?: string;
@ -389,16 +402,25 @@ type WorkflowNodeDropdwonItem = {
children?: WorkflowNodeDropdwonItem[]; children?: WorkflowNodeDropdwonItem[];
}; };
/**
* @deprecated
*/
export enum WorkflowNodeDropdwonItemIconType { export enum WorkflowNodeDropdwonItemIconType {
Icon, Icon,
Provider, Provider,
} }
/**
* @deprecated
*/
export type WorkflowNodeDropdwonItemIcon = { export type WorkflowNodeDropdwonItemIcon = {
type: WorkflowNodeDropdwonItemIconType; type: WorkflowNodeDropdwonItemIconType;
name: string; name: string;
}; };
/**
* @deprecated
*/
const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(deployProvidersMap.values()).map((item) => { const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(deployProvidersMap.values()).map((item) => {
return { return {
type: WorkflowNodeType.Apply, type: WorkflowNodeType.Apply,
@ -412,41 +434,44 @@ const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(de
}; };
}); });
/**
* @deprecated
*/
export const workflowNodeDropdownList: WorkflowNodeDropdwonItem[] = [ export const workflowNodeDropdownList: WorkflowNodeDropdwonItem[] = [
{ {
type: WorkflowNodeType.Apply, type: WorkflowNodeType.Apply,
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Apply) ?? "", name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Apply) ?? "",
icon: { icon: {
type: WorkflowNodeDropdwonItemIconType.Icon, type: WorkflowNodeDropdwonItemIconType.Icon,
name: "NotebookPen", name: "ApplyNodeIcon",
}, },
leaf: true, leaf: true,
}, },
{ {
type: WorkflowNodeType.Deploy, type: WorkflowNodeType.Deploy,
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Deploy) ?? "", name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Deploy) ?? "",
icon: { icon: {
type: WorkflowNodeDropdwonItemIconType.Icon, type: WorkflowNodeDropdwonItemIconType.Icon,
name: "CloudUpload", name: "DeployNodeIcon",
}, },
children: workflowNodeDropdownDeployList, children: workflowNodeDropdownDeployList,
}, },
{ {
type: WorkflowNodeType.Branch, type: WorkflowNodeType.Branch,
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Branch) ?? "", name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Branch) ?? "",
leaf: true, leaf: true,
icon: { icon: {
type: WorkflowNodeDropdwonItemIconType.Icon, type: WorkflowNodeDropdwonItemIconType.Icon,
name: "GitFork", name: "BranchNodeIcon",
}, },
}, },
{ {
type: WorkflowNodeType.Notify, type: WorkflowNodeType.Notify,
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Notify) ?? "", name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Notify) ?? "",
leaf: true, leaf: true,
icon: { icon: {
type: WorkflowNodeDropdwonItemIconType.Icon, type: WorkflowNodeDropdwonItemIconType.Icon,
name: "Megaphone", name: "NotifyNodeIcon",
}, },
}, },
]; ];

View File

@ -1,5 +1,3 @@
import { type WorkflowOutput } from "./workflow";
export interface WorkflowRunModel extends BaseModel { export interface WorkflowRunModel extends BaseModel {
workflow: string; workflow: string;
log: WorkflowRunLog[]; log: WorkflowRunLog[];
@ -10,5 +8,12 @@ export interface WorkflowRunModel extends BaseModel {
export type WorkflowRunLog = { export type WorkflowRunLog = {
nodeName: string; nodeName: string;
error: string; error: string;
outputs: WorkflowOutput[]; outputs: WorkflowRunLogOutput[];
};
export type WorkflowRunLogOutput = {
time: ISO8601String;
title: string;
content: string;
error: string;
}; };

4
ui/src/global.d.ts vendored
View File

@ -1,6 +1,8 @@
import { type BaseModel as PbBaseModel } from "pocketbase"; import { type BaseModel as PbBaseModel } from "pocketbase";
declare global { declare global {
declare type ISO8601String = string;
declare interface BaseModel extends PbBaseModel { declare interface BaseModel extends PbBaseModel {
created: ISO8601String; created: ISO8601String;
updated: ISO8601String; updated: ISO8601String;
@ -10,8 +12,6 @@ declare global {
declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, "id" | "created" | "updated" | "deleted">; declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, "id" | "created" | "updated" | "deleted">;
declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, "id">; declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, "id">;
declare type ISO8601String = string;
} }
export {}; export {};

View File

@ -3,7 +3,7 @@ import { initReactI18next } from "react-i18next";
import i18n from "i18next"; import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import resources, { LOCALE_ZH_NAME, LOCALE_EN_NAME } from "./locales"; import resources, { LOCALE_EN_NAME, LOCALE_ZH_NAME } from "./locales";
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)

View File

@ -3,9 +3,8 @@
"common.button.cancel": "Cancel", "common.button.cancel": "Cancel",
"common.button.copy": "Copy", "common.button.copy": "Copy",
"common.button.delete": "Delete", "common.button.delete": "Delete",
"common.button.disable": "Disable",
"common.button.edit": "Edit", "common.button.edit": "Edit",
"common.button.enable": "Enable", "common.button.more": "More",
"common.button.ok": "Ok", "common.button.ok": "Ok",
"common.button.reset": "Reset", "common.button.reset": "Reset",
"common.button.save": "Save", "common.button.save": "Save",

View File

@ -7,14 +7,9 @@
"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 changes", "workflow.action.enable": "Enable",
"workflow.action.discard.confirm": "Are you sure to discard your changes?",
"workflow.action.release": "Release",
"workflow.action.release.confirm": "Are you sure to release your changes?",
"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.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first",
"workflow.action.disable": "Disable",
"workflow.props.name": "Name", "workflow.props.name": "Name",
"workflow.props.description": "Description", "workflow.props.description": "Description",
@ -28,14 +23,28 @@
"workflow.props.created_at": "Created at", "workflow.props.created_at": "Created at",
"workflow.props.updated_at": "Updated at", "workflow.props.updated_at": "Updated at",
"workflow.detail.orchestration.tab": "Orchestration", "workflow.new.title": "Create Workflow",
"workflow.detail.runs.tab": "History runs", "workflow.new.subtitle": "Apply, deploy and notify with Workflows",
"workflow.new.templates.title": "Choose a Workflow Template",
"workflow.new.templates.template.standard.title": "Standard template",
"workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.",
"workflow.new.templates.template.blank.title": "Blank template",
"workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.",
"workflow.detail.baseinfo.modal.title": "Workflow base information", "workflow.detail.baseinfo.modal.title": "Workflow base information",
"workflow.detail.baseinfo.form.name.label": "Name", "workflow.detail.baseinfo.form.name.label": "Name",
"workflow.detail.baseinfo.form.name.placeholder": "Please enter name", "workflow.detail.baseinfo.form.name.placeholder": "Please enter name",
"workflow.detail.baseinfo.form.description.label": "Description", "workflow.detail.baseinfo.form.description.label": "Description",
"workflow.detail.baseinfo.form.description.placeholder": "Please enter description", "workflow.detail.baseinfo.form.description.placeholder": "Please enter description",
"workflow.detail.orchestration.tab": "Orchestration",
"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",
"workflow.detail.orchestration.action.release.confirm": "Are you sure to release your changes?",
"workflow.detail.orchestration.action.release.failed.uncompleted": "Please complete the orchestration first",
"workflow.detail.orchestration.action.run": "Run",
"workflow.detail.orchestration.action.run.confirm": "There are unreleased changes, are you sure to run this workflow based on the latest released version?",
"workflow.detail.runs.tab": "History runs",
"workflow.common.certificate.label": "Certificate", "workflow.common.certificate.label": "Certificate",
"workflow.node.setting.label": "Setting Node", "workflow.node.setting.label": "Setting Node",

View File

@ -3,9 +3,8 @@
"common.button.cancel": "取消", "common.button.cancel": "取消",
"common.button.copy": "复制", "common.button.copy": "复制",
"common.button.delete": "刪除", "common.button.delete": "刪除",
"common.button.disable": "禁用",
"common.button.edit": "编辑", "common.button.edit": "编辑",
"common.button.enable": "启用", "common.button.more": "更多",
"common.button.ok": "确定", "common.button.ok": "确定",
"common.button.reset": "重置", "common.button.reset": "重置",
"common.button.save": "保存", "common.button.save": "保存",

View File

@ -7,14 +7,9 @@
"workflow.action.edit": "编辑工作流", "workflow.action.edit": "编辑工作流",
"workflow.action.delete": "删除工作流", "workflow.action.delete": "删除工作流",
"workflow.action.delete.confirm": "确定要删除此工作流吗?", "workflow.action.delete.confirm": "确定要删除此工作流吗?",
"workflow.action.discard": "撤销更改", "workflow.action.enable": "启用",
"workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
"workflow.action.release": "发布更改",
"workflow.action.release.confirm": "确定要发布更改吗?",
"workflow.action.release.failed.uncompleted": "请先完成流程编排",
"workflow.action.run": "执行",
"workflow.action.run.confirm": "存在未发布的更改,确定要按最近一次发布的版本来执行此工作流吗?",
"workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改", "workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改",
"workflow.action.disable": "禁用",
"workflow.props.name": "名称", "workflow.props.name": "名称",
"workflow.props.description": "描述", "workflow.props.description": "描述",
@ -28,14 +23,28 @@
"workflow.props.created_at": "创建时间", "workflow.props.created_at": "创建时间",
"workflow.props.updated_at": "更新时间", "workflow.props.updated_at": "更新时间",
"workflow.detail.orchestration.tab": "流程编排", "workflow.new.title": "新建工作流",
"workflow.detail.runs.tab": "执行历史", "workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知",
"workflow.new.templates.title": "选择工作流模板",
"workflow.new.templates.template.standard.title": "标准模板",
"workflow.new.templates.template.standard.description": "一个包含申请 + 部署 + 通知步骤的标准工作流程。",
"workflow.new.templates.template.blank.title": "空白模板",
"workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。",
"workflow.detail.baseinfo.modal.title": "编辑基本信息", "workflow.detail.baseinfo.modal.title": "编辑基本信息",
"workflow.detail.baseinfo.form.name.label": "名称", "workflow.detail.baseinfo.form.name.label": "名称",
"workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称", "workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称",
"workflow.detail.baseinfo.form.description.label": "描述", "workflow.detail.baseinfo.form.description.label": "描述",
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述", "workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
"workflow.detail.orchestration.tab": "流程编排",
"workflow.detail.orchestration.action.discard": "撤销更改",
"workflow.detail.orchestration.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
"workflow.detail.orchestration.action.release": "发布更改",
"workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?",
"workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未设置",
"workflow.detail.orchestration.action.run": "执行",
"workflow.detail.orchestration.action.run.confirm": "此工作流存在未发布的更改,将以最近一次发布的版本为准,确定要继续执行吗?",
"workflow.detail.runs.tab": "执行历史",
"workflow.common.certificate.label": "证书", "workflow.common.certificate.label": "证书",
"workflow.node.setting.label": "设置节点", "workflow.node.setting.label": "设置节点",

View File

@ -5,6 +5,7 @@ import {
ApartmentOutlined as ApartmentOutlinedIcon, ApartmentOutlined as ApartmentOutlinedIcon,
CaretRightOutlined as CaretRightOutlinedIcon, CaretRightOutlined as CaretRightOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
DownOutlined as DownOutlinedIcon,
EllipsisOutlined as EllipsisOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon,
HistoryOutlined as HistoryOutlinedIcon, HistoryOutlined as HistoryOutlinedIcon,
UndoOutlined as UndoOutlinedIcon, UndoOutlined as UndoOutlinedIcon,
@ -45,8 +46,8 @@ const WorkflowDetail = () => {
); );
useEffect(() => { useEffect(() => {
// TODO: loading // TODO: loading
init(workflowId); init(workflowId!);
}, [workflowId, init]); }, [workflowId]);
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
@ -70,10 +71,13 @@ const WorkflowDetail = () => {
const [allowDiscard, setAllowDiscard] = useState(false); const [allowDiscard, setAllowDiscard] = useState(false);
const [allowRelease, setAllowRelease] = useState(false); const [allowRelease, setAllowRelease] = useState(false);
const [allowRun, setAllowRun] = useState(false);
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
const hasReleased = !!workflow.content;
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content); const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
setAllowDiscard(hasChanges && !workflowRunning); setAllowDiscard(!workflowRunning && hasReleased && hasChanges);
setAllowRelease(hasChanges && !workflowRunning); setAllowRelease(!workflowRunning && hasChanges);
setAllowRun(hasReleased);
}, [workflow, workflowRunning]); }, [workflow, workflowRunning]);
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => { const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
@ -86,13 +90,18 @@ const WorkflowDetail = () => {
} }
}; };
const handleEnableChange = () => { const handleEnableChange = async () => {
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
messageApi.warning(t("workflow.action.enable.failed.uncompleted")); messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
return; return;
} }
switchEnable(); try {
await switchEnable();
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
}; };
const handleDeleteClick = () => { const handleDeleteClick = () => {
@ -114,18 +123,24 @@ const WorkflowDetail = () => {
}; };
const handleDiscardClick = () => { const handleDiscardClick = () => {
alert("TODO"); modalApi.confirm({
title: t("workflow.detail.orchestration.action.discard"),
content: t("workflow.detail.orchestration.action.discard.confirm"),
onOk: () => {
alert("TODO");
},
});
}; };
const handleReleaseClick = () => { const handleReleaseClick = () => {
if (!isAllNodesValidated(workflow.draft!)) { if (!isAllNodesValidated(workflow.draft!)) {
messageApi.warning(t("workflow.action.release.failed.uncompleted")); messageApi.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted"));
return; return;
} }
modalApi.confirm({ modalApi.confirm({
title: t("workflow.action.release"), title: t("workflow.detail.orchestration.action.release"),
content: t("workflow.action.release.confirm"), content: t("workflow.detail.orchestration.action.release.confirm"),
onOk: async () => { onOk: async () => {
try { try {
await save(); await save();
@ -148,8 +163,8 @@ const WorkflowDetail = () => {
const { promise, resolve, reject } = Promise.withResolvers(); const { promise, resolve, reject } = Promise.withResolvers();
if (workflow.hasDraft) { if (workflow.hasDraft) {
modalApi.confirm({ modalApi.confirm({
title: t("workflow.action.run"), title: t("workflow.detail.orchestration.action.run"),
content: t("workflow.action.run.confirm"), content: t("workflow.detail.orchestration.action.run.confirm"),
onOk: () => resolve(void 0), onOk: () => resolve(void 0),
onCancel: () => reject(), onCancel: () => reject(),
}); });
@ -164,7 +179,7 @@ const WorkflowDetail = () => {
try { try {
await runWorkflow(workflowId!); await runWorkflow(workflowId!);
messageApi.warning(t("common.text.operation_succeeded")); messageApi.success(t("common.text.operation_succeeded"));
} catch (err) { } catch (err) {
if (err instanceof ClientResponseError && err.isAbort) { if (err instanceof ClientResponseError && err.isAbort) {
return; return;
@ -189,30 +204,33 @@ const WorkflowDetail = () => {
style={{ paddingBottom: 0 }} style={{ paddingBottom: 0 }}
title={workflow.name} title={workflow.name}
extra={[ extra={[
<Button.Group key="actions"> <WorkflowBaseInfoModalForm key="edit" data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />,
<WorkflowBaseInfoModalForm data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />
<Button onClick={handleEnableChange}>{workflow.enabled ? t("common.button.disable") : t("common.button.enable")}</Button> <Button key="enable" onClick={handleEnableChange}>
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
</Button>,
<Dropdown <Dropdown
menu={{ key="more"
items: [ menu={{
{ items: [
key: "delete", {
label: t("common.button.delete"), key: "delete",
danger: true, label: t("workflow.action.delete"),
icon: <DeleteOutlinedIcon />, danger: true,
onClick: () => { icon: <DeleteOutlinedIcon />,
handleDeleteClick(); onClick: () => {
}, handleDeleteClick();
}, },
], },
}} ],
trigger={["click"]} }}
> trigger={["click"]}
<Button icon={<EllipsisOutlinedIcon />} /> >
</Dropdown> <Button icon={<DownOutlinedIcon />} iconPosition="end">
</Button.Group>, {t("common.button.more")}
</Button>
</Dropdown>,
]} ]}
> >
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph> <Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
@ -239,13 +257,13 @@ const WorkflowDetail = () => {
</div> </div>
<div className="absolute top-0 right-0 z-[1]"> <div className="absolute top-0 right-0 z-[1]">
<Space> <Space>
<Button icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}> <Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
{t("workflow.action.run")} {t("workflow.detail.orchestration.action.run")}
</Button> </Button>
<Button.Group> <Button.Group>
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}> <Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
{t("workflow.action.release")} {t("workflow.detail.orchestration.action.release")}
</Button> </Button>
<Dropdown <Dropdown
@ -254,7 +272,7 @@ const WorkflowDetail = () => {
{ {
key: "discard", key: "discard",
disabled: !allowDiscard, disabled: !allowDiscard,
label: t("workflow.action.discard"), label: t("workflow.detail.orchestration.action.discard"),
icon: <UndoOutlinedIcon />, icon: <UndoOutlinedIcon />,
onClick: handleDiscardClick, onClick: handleDiscardClick,
}, },

View File

@ -245,7 +245,7 @@ const WorkflowList = () => {
const handleEnabledChange = async (workflow: WorkflowModel) => { const handleEnabledChange = async (workflow: WorkflowModel) => {
try { try {
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
messageApi.warning(t("workflow.action.enable.failed.uncompleted")); messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
return; return;
} }

View File

@ -0,0 +1,123 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { PageHeader } from "@ant-design/pro-components";
import { Card, Col, Row, Spin, Typography, notification } from "antd";
import { sleep } from "radash";
import { type WorkflowModel, initWorkflow } from "@/domain/workflow";
import { save as saveWorkflow } from "@/repository/workflow";
import { getErrMsg } from "@/utils/error";
const TEMPLATE_KEY_BLANK = "blank" as const;
const TEMPLATE_KEY_STANDARD = "standard" as const;
type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD;
const WorkflowNew = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const templateGridSpans = {
xs: { flex: "100%" },
md: { flex: "100%" },
lg: { flex: "50%" },
xl: { flex: "50%" },
xxl: { flex: "50%" },
};
const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();
const handleTemplateSelect = async (key: TemplateKeys) => {
if (templateSelectKey) return;
setTemplateSelectKey(key);
try {
let workflow: WorkflowModel;
switch (key) {
case TEMPLATE_KEY_BLANK:
workflow = initWorkflow();
break;
case TEMPLATE_KEY_STANDARD:
workflow = initWorkflow({ template: "standard" });
break;
default:
throw "Invalid args: `key`";
}
workflow = await saveWorkflow(workflow);
await sleep(500);
await navigate(`/workflows/${workflow.id}`, { replace: true });
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
setTemplateSelectKey(undefined);
}
};
return (
<div>
{NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
<PageHeader title={t("workflow.new.title")}>
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
</PageHeader>
</Card>
<div className="p-4">
<div className="max-w-[960px] mx-auto px-2">
<Typography.Text type="secondary">
<div className="mt-4 mb-8 text-xl">{t("workflow.new.templates.title")}</div>
</Typography.Text>
<Row className="justify-stretch" gutter={[16, 16]}>
<Col {...templateGridSpans}>
<Card
className="size-full"
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-standard.png" />}
hoverable
onClick={() => handleTemplateSelect(TEMPLATE_KEY_STANDARD)}
>
<div className="flex items-center gap-4 w-full">
<Card.Meta
className="flex-grow"
title={t("workflow.new.templates.template.standard.title")}
description={t("workflow.new.templates.template.standard.description")}
/>
<Spin spinning={templateSelectKey === TEMPLATE_KEY_STANDARD} />
</div>
</Card>
</Col>
<Col {...templateGridSpans}>
<Card
className="size-full"
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-blank.png" />}
hoverable
onClick={() => handleTemplateSelect(TEMPLATE_KEY_BLANK)}
>
<div className="flex items-center gap-4 w-full">
<Card.Meta
className="flex-grow"
title={t("workflow.new.templates.template.blank.title")}
description={t("workflow.new.templates.template.blank.description")}
/>
<Spin spinning={templateSelectKey === TEMPLATE_KEY_BLANK} />
</div>
</Card>
</Col>
</Row>
</div>
</div>
</div>
);
};
export default WorkflowNew;

View File

@ -1,18 +1,16 @@
import { create } from "zustand"; import { create } from "zustand";
import { import {
type WorkflowBranchNode,
type WorkflowModel,
type WorkflowNode,
addBranch, addBranch,
addNode, addNode,
getExecuteMethod, getExecuteMethod,
getWorkflowOutputBeforeId, getWorkflowOutputBeforeId,
initWorkflow,
removeBranch, removeBranch,
removeNode, removeNode,
updateNode, updateNode,
type WorkflowBranchNode,
type WorkflowModel,
type WorkflowNode,
WorkflowNodeType,
} from "@/domain/workflow"; } from "@/domain/workflow";
import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow";
@ -27,44 +25,29 @@ export type WorkflowState = {
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
switchEnable(): void; switchEnable(): void;
save(): void; save(): void;
init(id?: string): void; init(id: string): void;
setBaseInfo: (name: string, description: string) => void; setBaseInfo: (name: string, description: string) => void;
}; };
export const useWorkflowStore = create<WorkflowState>((set, get) => ({ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
workflow: { workflow: {} as WorkflowModel,
id: "",
name: "",
type: WorkflowNodeType.Start,
} as WorkflowModel,
initialized: false, initialized: false,
init: async (id?: string) => {
let data = {
id: "",
name: "",
type: "auto",
} as WorkflowModel;
if (!id) { init: async (id: string) => {
data = initWorkflow(); const data = await getWorkflow(id);
} else {
data = await getWorkflow(id);
}
set({ set({
workflow: data, workflow: data,
initialized: true, initialized: true,
}); });
}, },
setBaseInfo: async (name: string, description: string) => { setBaseInfo: async (name: string, description: string) => {
const data: Record<string, string | boolean | WorkflowNode> = { const data: Record<string, string | boolean | WorkflowNode> = {
id: (get().workflow.id as string) ?? "", id: (get().workflow.id as string) ?? "",
name: name || "", name: name || "",
description: description || "", description: description || "",
}; };
if (!data.id) {
data.draft = get().workflow.draft as WorkflowNode;
}
const resp = await saveWorkflow(data); const resp = await saveWorkflow(data);
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
@ -77,17 +60,18 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
switchEnable: async () => { switchEnable: async () => {
const root = get().workflow.draft as WorkflowNode; const root = get().workflow.content as WorkflowNode;
const executeMethod = getExecuteMethod(root); const executeMethod = getExecuteMethod(root);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: (get().workflow.id as string) ?? "",
content: root, content: root,
enabled: !get().workflow.enabled, enabled: !get().workflow.enabled,
hasDraft: false,
type: executeMethod.type, type: executeMethod.type,
crontab: executeMethod.crontab, crontab: executeMethod.crontab,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -95,13 +79,13 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
id: resp.id, id: resp.id,
content: resp.content, content: resp.content,
enabled: resp.enabled, enabled: resp.enabled,
hasDraft: false,
type: resp.type, type: resp.type,
crontab: resp.crontab, crontab: resp.crontab,
}, },
}; };
}); });
}, },
save: async () => { save: async () => {
const root = get().workflow.draft as WorkflowNode; const root = get().workflow.draft as WorkflowNode;
const executeMethod = getExecuteMethod(root); const executeMethod = getExecuteMethod(root);
@ -112,6 +96,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
type: executeMethod.type, type: executeMethod.type,
crontab: executeMethod.crontab, crontab: executeMethod.crontab,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -125,6 +110,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
updateNode: async (node: WorkflowNode | WorkflowBranchNode) => { updateNode: async (node: WorkflowNode | WorkflowBranchNode) => {
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node); const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
@ -132,6 +118,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
draft: newRoot, draft: newRoot,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -143,6 +130,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => { addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => {
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node); const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
@ -150,6 +138,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
draft: newRoot, draft: newRoot,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -161,6 +150,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
addBranch: async (branchId: string) => { addBranch: async (branchId: string) => {
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId); const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
@ -168,6 +158,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
draft: newRoot, draft: newRoot,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -179,6 +170,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
removeBranch: async (branchId: string, index: number) => { removeBranch: async (branchId: string, index: number) => {
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index); const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
@ -186,6 +178,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
draft: newRoot, draft: newRoot,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -197,6 +190,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
removeNode: async (nodeId: string) => { removeNode: async (nodeId: string) => {
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId); const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
@ -204,6 +198,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
draft: newRoot, draft: newRoot,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: {
@ -215,6 +210,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}; };
}); });
}, },
getWorkflowOuptutBeforeId: (id: string, type: string) => { getWorkflowOuptutBeforeId: (id: string, type: string) => {
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
}, },