refactor(ui): optimize stores

This commit is contained in:
Fu Diwei
2025-07-29 14:37:06 +08:00
parent cd68c49a4b
commit 396be6f9d5
12 changed files with 423 additions and 399 deletions
+4 -6
View File
@@ -28,7 +28,7 @@
"workflow.action.rollback.button": "Rollback",
"workflow.action.rollback.modal.title": "Rollback changes",
"workflow.action.rollback.modal.content": "Are you sure to rollback your changes to the last published version?",
"workflow.action.run.button": "Run",
"workflow.action.run.button": "Run at once",
"workflow.action.run.modal.title": "Run workflow",
"workflow.action.run.modal.content": "You have unpublished changes. Do you really want to run this workflow based on the last published version?",
"workflow.action.run.prompt": "Running... Please check the history later",
@@ -58,12 +58,10 @@
"workflow.new.templates.default_name": "Untitled workflow",
"workflow.new.templates.default_description": "Created at {{date}}",
"workflow.detail.baseinfo.modal.title": "Workflow base information",
"workflow.detail.baseinfo.form.name.label": "Name",
"workflow.detail.baseinfo.form.name.placeholder": "Please enter workflow name",
"workflow.detail.baseinfo.form.description.label": "Description (Optional)",
"workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description",
"workflow.detail.baseinfo.name.placeholder": "Please enter workflow name",
"workflow.detail.baseinfo.description.placeholder": "Please enter workflow description",
"workflow.detail.design.tab": "Orchestration",
"workflow.detail.design_legacy.tab": "Orchestration (Legacy)",
"workflow.detail.design.draft.alert": "The orchestration is not released yet.",
"workflow.detail.runs.tab": "History runs"
}
+5 -7
View File
@@ -28,7 +28,7 @@
"workflow.action.rollback.button": "回退更改",
"workflow.action.rollback.modal.title": "回退更改",
"workflow.action.rollback.modal.content": "确定要回退到最近一次发布的版本吗?",
"workflow.action.run.button": "执行",
"workflow.action.run.button": "立即执行",
"workflow.action.run.modal.title": "执行工作流",
"workflow.action.run.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
"workflow.action.run.prompt": "执行中……请稍后查看执行历史",
@@ -39,7 +39,7 @@
"workflow.props.trigger.scheduled": "定时",
"workflow.props.trigger.manual": "手动",
"workflow.props.last_run_at": "最近执行时间",
"workflow.props.state": "启用",
"workflow.props.state": "是否启用",
"workflow.props.state.filter.all": "全部",
"workflow.props.state.filter.enabled": "启用",
"workflow.props.state.filter.disabled": "未启用",
@@ -58,12 +58,10 @@
"workflow.new.templates.default_name": "未命名工作流",
"workflow.new.templates.default_description": "创建于 {{date}}",
"workflow.detail.baseinfo.modal.title": "编辑基本信息",
"workflow.detail.baseinfo.form.name.label": "名称",
"workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称",
"workflow.detail.baseinfo.form.description.label": "描述(可选)",
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
"workflow.detail.baseinfo.name.placeholder": "请输入工作流名称",
"workflow.detail.baseinfo.description.placeholder": "请输入工作流描述",
"workflow.detail.design.tab": "流程编排",
"workflow.detail.design_legacy.tab": "旧版编辑器",
"workflow.detail.design.draft.alert": "当前编排有未发布的更改。",
"workflow.detail.runs.tab": "执行历史"
}
+1 -1
View File
@@ -16,5 +16,5 @@ body {
min-width: 360px;
min-height: 540px;
min-height: 100vh;
min-height: calc(min(360px, 100vh));
min-height: calc(min(540px, 100vh));
}
+3 -3
View File
@@ -16,7 +16,7 @@ const Settings = () => {
["ssl-provider", "settings.sslprovider.tab", <IconPlugConnected size="1em" />],
["persistence", "settings.persistence.tab", <IconDatabaseCog size="1em" />],
] satisfies [string, string, React.ReactElement][];
const [menuKey, setMenuValue] = useState<string>();
const [menuKey, setMenuKey] = useState<string>(menus[0][0]);
useEffect(() => {
const subpath = location.pathname.split("/")[2];
if (!subpath) {
@@ -24,11 +24,11 @@ const Settings = () => {
return;
}
setMenuValue(subpath);
setMenuKey(subpath);
}, [location.pathname]);
const handleMenuClick = ({ key }: { key: string }) => {
setMenuValue(key);
setMenuKey(key);
navigate(`/settings/${key}`);
};
+88 -29
View File
@@ -1,10 +1,13 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
import { IconEdit, IconHistory, IconRobot } from "@tabler/icons-react";
import { App, Button, Input, type InputRef, Segmented, Skeleton } from "antd";
import { IconEdit, IconHistory, IconPlayerPlay, IconRobot } from "@tabler/icons-react";
import { useSize } from "ahooks";
import { App, Button, Input, type InputRef, Segmented, Skeleton, Space } from "antd";
import { startRun as startWorkflowRun } from "@/api/workflows";
import Show from "@/components/Show";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import { mergeCls } from "@/utils/css";
@@ -16,6 +19,8 @@ const WorkflowDetail = () => {
const { t } = useTranslation();
const { message, modal } = App.useApp();
const { id: workflowId } = useParams();
const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "init", "destroy"]));
useEffect(() => {
@@ -26,11 +31,14 @@ const WorkflowDetail = () => {
};
}, [workflowId]);
const divHeaderRef = useRef<HTMLDivElement>(null);
const divHeaderSize = useSize(divHeaderRef);
const tabs = [
["design", "workflow.detail.design.tab", <IconRobot size="1em" />],
["runs", "workflow.detail.runs.tab", <IconHistory size="1em" />],
] satisfies [string, string, React.ReactElement][];
const [tabValue, setTabValue] = useState<string>();
const [tabValue, setTabValue] = useState<string>(tabs[0][0]);
useEffect(() => {
const subpath = location.pathname.split("/")[3];
if (!subpath) {
@@ -44,39 +52,86 @@ const WorkflowDetail = () => {
const handleTabChange = (value: string) => {
setTabValue(value);
navigate(`/workflows/${workflowId}/${value}`);
};
workflowState.init(workflow.id); // reload state
const runButtonDisabled = useMemo(() => !workflow?.content, [workflow]);
const [runButtonLoading, setRunButtonLoading] = useState(false);
useEffect(() => {
const pending = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING;
setRunButtonLoading(pending);
}, [workflow.lastRunStatus]);
const handleRunClick = () => {
const { promise, resolve, reject } = Promise.withResolvers();
if (workflow.hasDraft) {
modal.confirm({
title: t("workflow.action.run.modal.title"),
content: t("workflow.action.run.modal.content"),
onOk: () => resolve(void 0),
onCancel: () => reject(),
});
} else {
resolve(void 0);
}
promise.then(async () => {
try {
setRunButtonLoading(true);
await startWorkflowRun(workflow.id);
message.info(t("workflow.action.run.prompt"));
} catch (err) {
setRunButtonLoading(false);
console.error(err);
message.warning(t("common.text.operation_failed"));
}
});
};
return (
<div className="flex size-full flex-col">
<div className="px-6 py-4">
<div className="relative mx-auto max-w-320">
<WorkflowDetailBaseName />
<WorkflowDetailBaseDescription />
<div className="px-6 py-4" ref={divHeaderRef}>
<div className="relative z-11 mx-auto flex max-w-320 justify-between gap-4">
<div className="flex-1">
<WorkflowDetailBaseName />
<WorkflowDetailBaseDescription />
<div className="absolute -bottom-12 left-1/2 z-1 -translate-x-1/2">
<Segmented
className="shadow"
options={tabs.map(([key, label, icon]) => ({
value: key,
label: <span className="px-2 text-sm">{t(label)}</span>,
icon: (
<span className="anticon scale-125" role="img">
{icon}
</span>
),
}))}
size="large"
value={tabValue}
defaultValue="design"
onChange={handleTabChange}
/>
<div className="absolute -bottom-12 left-1/2 z-1 -translate-x-1/2">
<Segmented
className="shadow"
options={tabs.map(([key, label, icon]) => ({
value: key,
label: <span className="px-2 text-sm">{t(label)}</span>,
icon: (
<span className="anticon scale-125" role="img">
{icon}
</span>
),
}))}
size="large"
value={tabValue}
onChange={handleTabChange}
/>
</div>
</div>
<div className="py-2">
<Space.Compact>
<Button disabled={runButtonDisabled} icon={<IconPlayerPlay size="1.25em" />} loading={runButtonLoading} type="primary" onClick={handleRunClick}>
{t("workflow.action.run.button")}
</Button>
</Space.Compact>
</div>
</div>
</div>
<div className="p-4">
<div
className="flex-1 p-4"
style={{
minHeight: `calc(max(360px, 100% - ${divHeaderSize?.height ?? 0}px))`,
}}
>
<Show
when={initialized}
fallback={
@@ -141,7 +196,7 @@ const WorkflowDetailBaseName = () => {
return (
<div className="group relative flex items-center gap-1">
<h1
className={mergeCls({
className={mergeCls("break-all", {
invisible: editing,
})}
>
@@ -158,6 +213,8 @@ const WorkflowDetailBaseName = () => {
<Input
className={mergeCls("absolute top-0 left-0", editing ? "block" : "hidden")}
ref={inputRef}
maxLength={100}
placeholder={t("workflow.detail.baseinfo.name.placeholder")}
size="large"
value={value}
variant="filled"
@@ -199,7 +256,7 @@ const WorkflowDetailBaseDescription = () => {
const handleValueConfirm = async (value: string) => {
value = value.trim();
if (value === (workflow.description || "")) {
if (!value || value === (workflow.description || "")) {
setEditing(false);
return;
}
@@ -235,6 +292,8 @@ const WorkflowDetailBaseDescription = () => {
<Input
className={mergeCls("absolute top-0 left-0", editing ? "block" : "hidden")}
ref={inputRef}
maxLength={100}
placeholder={t("workflow.detail.baseinfo.description.placeholder")}
value={value}
variant="filled"
onBlur={(e) => handleValueConfirm(e.target.value)}
+22 -89
View File
@@ -1,16 +1,14 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconArrowBackUp, IconDots, IconPlayerPlay } from "@tabler/icons-react";
import { IconArrowBackUp, IconDots } from "@tabler/icons-react";
import { Alert, App, Button, Card, Dropdown, Space } from "antd";
import { isEqual } from "radash";
import { startRun as startWorkflowRun } from "@/api/workflows";
import Show from "@/components/Show";
import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useZustandShallowSelector } from "@/hooks";
import { subscribe as subscribeWorkflow } from "@/repository/workflow";
import { useWorkflowStore } from "@/stores/workflow";
import { getErrMsg } from "@/utils/error";
@@ -24,37 +22,17 @@ const WorkflowDetailDesign = () => {
const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
const [allowRollback, setAllowRollback] = useState(false);
const [allowPublish, setAllowPublish] = useState(false);
const [allowRun, setAllowRun] = useState(false);
useEffect(() => {
const pending = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING;
setIsPendingOrRunning(pending);
}, [workflow]);
useEffect(() => {
if (isPendingOrRunning) {
let unsubscribeFn: Awaited<ReturnType<typeof subscribeWorkflow>> | undefined = undefined;
subscribeWorkflow(workflow.id, (cb) => {
if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
setIsPendingOrRunning(false);
unsubscribeFn?.();
}
}).then((res) => {
unsubscribeFn = res;
});
return () => {
unsubscribeFn?.();
};
}
}, [workflow.id, isPendingOrRunning]);
useEffect(() => {
const hasContent = !!workflow.content;
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
setAllowRollback(!isPendingOrRunning && hasContent && hasChanges);
setAllowPublish(!isPendingOrRunning && hasChanges);
setAllowRun(hasContent);
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
const handleRollbackClick = () => {
@@ -96,46 +74,6 @@ const WorkflowDetailDesign = () => {
});
};
const handleRunClick = () => {
const { promise, resolve, reject } = Promise.withResolvers();
if (workflow.hasDraft) {
modal.confirm({
title: t("workflow.action.run.modal.title"),
content: t("workflow.action.run.modal.content"),
onOk: () => resolve(void 0),
onCancel: () => reject(),
});
} else {
resolve(void 0);
}
promise.then(async () => {
let unsubscribeFn: Awaited<ReturnType<typeof subscribeWorkflow>> | undefined = undefined;
try {
setIsPendingOrRunning(true);
// subscribe before running workflow
unsubscribeFn = await subscribeWorkflow(workflow.id, (e) => {
if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
setIsPendingOrRunning(false);
unsubscribeFn?.();
}
});
await startWorkflowRun(workflow.id);
message.info(t("workflow.action.run.prompt"));
} catch (err) {
setIsPendingOrRunning(false);
unsubscribeFn?.();
console.error(err);
message.warning(t("common.text.operation_failed"));
}
});
};
return (
<div className="min-h-[360px] flex-1 overflow-hidden">
<Card
@@ -148,7 +86,7 @@ const WorkflowDetailDesign = () => {
},
}}
>
<div className="pt-9">
<div className="size-full pt-9">
<div className="absolute inset-x-6 z-2 mx-auto flex max-w-320 items-center justify-between gap-4">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
@@ -156,32 +94,27 @@ const WorkflowDetailDesign = () => {
</Show>
</div>
<div className="flex justify-end">
<Space>
<Button disabled={!allowRun} icon={<IconPlayerPlay size="1.25em" />} loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
{t("workflow.action.run.button")}
<Space.Compact>
<Button color="primary" disabled={!allowPublish} variant="outlined" onClick={handlePublishClick}>
{t("workflow.action.publish.button")}
</Button>
<Space.Compact>
<Button color="primary" disabled={!allowPublish} variant="outlined" onClick={handlePublishClick}>
{t("workflow.action.publish.button")}
</Button>
<Dropdown
menu={{
items: [
{
key: "rollback",
disabled: !allowRollback,
label: t("workflow.action.rollback.button"),
icon: <IconArrowBackUp size="1.25em" />,
onClick: handleRollbackClick,
},
],
}}
trigger={["click"]}
>
<Button color="primary" icon={<IconDots size="1.25em" />} variant="outlined" />
</Dropdown>
</Space.Compact>
</Space>
<Dropdown
menu={{
items: [
{
key: "rollback",
disabled: !allowRollback,
label: t("workflow.action.rollback.button"),
icon: <IconArrowBackUp size="1.25em" />,
onClick: handleRollbackClick,
},
],
}}
trigger={["click"]}
>
<Button color="primary" icon={<IconDots size="1.25em" />} variant="outlined" />
</Dropdown>
</Space.Compact>
</div>
</div>
+2 -11
View File
@@ -4,18 +4,9 @@ import { create } from "zustand";
import { type AccessModel } from "@/domain/access";
import { list as listAccesses, remove as removeAccess, save as saveAccess } from "@/repository/access";
export interface AccessesState {
accesses: AccessModel[];
loading: boolean;
loadedAtOnce: boolean;
import { type AccessesState, type AccessesStore } from "./types";
fetchAccesses: () => Promise<void>;
createAccess: (access: MaybeModelRecord<AccessModel>) => Promise<AccessModel>;
updateAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<AccessModel>;
deleteAccess: (access: MaybeModelRecordWithId<AccessModel> | MaybeModelRecordWithId<AccessModel>[]) => Promise<AccessModel>;
}
export const useAccessesStore = create<AccessesState>((set) => {
export const useAccessesStore = create<AccessesStore>((set) => {
let fetcher: Promise<AccessModel[]> | null = null; // 防止多次重复请求
return {
+16
View File
@@ -0,0 +1,16 @@
import { type AccessModel } from "@/domain/access";
export interface AccessesState {
accesses: AccessModel[];
loading: boolean;
loadedAtOnce: boolean;
}
export interface AccessesActions {
fetchAccesses: () => Promise<void>;
createAccess: (access: MaybeModelRecord<AccessModel>) => Promise<AccessModel>;
updateAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<AccessModel>;
deleteAccess: (access: MaybeModelRecordWithId<AccessModel> | MaybeModelRecordWithId<AccessModel>[]) => Promise<AccessModel>;
}
export interface AccessesStore extends AccessesState, AccessesActions {}
+2 -11
View File
@@ -4,18 +4,9 @@ import { create } from "zustand";
import { type EmailsSettingsContent, SETTINGS_NAMES, type SettingsModel } from "@/domain/settings";
import { get as getSettings, save as saveSettings } from "@/repository/settings";
export interface ContactEmailsState {
emails: string[];
loading: boolean;
loadedAtOnce: boolean;
import { type ContactEmailsState, type ContactEmailsStore } from "./types";
fetchEmails: () => Promise<void>;
setEmails: (emails: string[]) => Promise<void>;
addEmail: (email: string) => Promise<void>;
removeEmail: (email: string) => Promise<void>;
}
export const useContactEmailsStore = create<ContactEmailsState>((set, get) => {
export const useContactEmailsStore = create<ContactEmailsStore>((set, get) => {
let fetcher: Promise<SettingsModel<EmailsSettingsContent>> | null = null; // 防止多次重复请求
let settings: SettingsModel<EmailsSettingsContent>; // 记录当前设置的其他字段,保存回数据库时用
+14
View File
@@ -0,0 +1,14 @@
export interface ContactEmailsState {
emails: string[];
loading: boolean;
loadedAtOnce: boolean;
}
export interface ContactEmailsActions {
fetchEmails: () => Promise<void>;
setEmails: (emails: string[]) => Promise<void>;
addEmail: (email: string) => Promise<void>;
removeEmail: (email: string) => Promise<void>;
}
export interface ContactEmailsStore extends ContactEmailsState, ContactEmailsActions {}
+238 -242
View File
@@ -3,7 +3,6 @@ import { create } from "zustand";
import {
type WorkflowModel,
type WorkflowNode,
type WorkflowNodeConfigForStart,
addBranch,
addNode,
@@ -14,280 +13,277 @@ import {
removeNode,
updateNode,
} from "@/domain/workflow";
import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow";
import { get as getWorkflow, save as saveWorkflow, subscribe as subscribeWorkflow } from "@/repository/workflow";
export type WorkflowState = {
workflow: WorkflowModel;
initialized: boolean;
import { type WorkflowState, type WorkflowStore } from "./types";
init(id: string): void;
setBaseInfo: (name: string, description: string) => void;
setEnabled(enabled: boolean): void;
publish(): void;
rollback(): void;
destroy(): void;
addNode: (node: WorkflowNode, previousNodeId: string) => void;
duplicateNode: (node: WorkflowNode) => void;
updateNode: (node: WorkflowNode) => void;
removeNode: (node: WorkflowNode) => void;
addBranch: (branchId: string) => void;
duplicateBranch: (branchId: string, index: number) => void;
removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[];
};
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
workflow: {} as WorkflowModel,
initialized: false,
init: async (id: string) => {
const data = await getWorkflow(id);
set({
workflow: data,
initialized: true,
});
},
destroy: () => {
set({
workflow: {} as WorkflowModel,
initialized: false,
});
},
setBaseInfo: async (name: string, description: string) => {
export const useWorkflowStore = create<WorkflowStore>((set, get) => {
const ensureInitialized = () => {
if (!get().initialized) throw "Workflow not initialized yet";
};
const resp = await saveWorkflow({
id: get().workflow.id!,
name: name || "",
description: description || "",
});
let unsubscriber: (() => void) | undefined = undefined;
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.name = resp.name;
draft.description = resp.description;
}),
};
});
},
return {
workflow: {} as WorkflowModel,
initialized: false,
setEnabled: async (enabled: boolean) => {
if (!get().initialized) throw "Workflow not initialized yet";
init: async (id: string) => {
const data = await getWorkflow(id);
set({
workflow: data,
initialized: true,
});
const resp = await saveWorkflow({
id: get().workflow.id!,
enabled: enabled,
});
unsubscriber ??= await subscribeWorkflow(id, (cb) => {
if (cb.record.id !== get().workflow.id) return;
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.enabled = resp.enabled;
}),
};
});
},
set({
workflow: cb.record,
});
});
},
publish: async () => {
if (!get().initialized) throw "Workflow not initialized yet";
destroy: () => {
unsubscriber?.();
unsubscriber = undefined;
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,
});
set({
workflow: {} as WorkflowModel,
initialized: false,
});
},
set((state: WorkflowState) => {
return {
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;
}),
};
});
},
setBaseInfo: async (name, description) => {
ensureInitialized();
rollback: async () => {
if (!get().initialized) throw "Workflow not initialized yet";
const resp = await saveWorkflow({
id: get().workflow.id!,
name: name || "",
description: description || "",
});
const root = get().workflow.content!;
const startConfig = root.config as WorkflowNodeConfigForStart;
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: false,
trigger: startConfig.trigger,
triggerCron: startConfig.triggerCron,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.name = resp.name;
draft.description = resp.description;
}),
};
});
},
set((state: WorkflowState) => {
return {
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;
}),
};
});
},
setEnabled: async (enabled) => {
ensureInitialized();
addNode: async (node: WorkflowNode, previousNodeId: string) => {
if (!get().initialized) throw "Workflow not initialized yet";
const resp = await saveWorkflow({
id: get().workflow.id!,
enabled: enabled,
});
const root = addNode(get().workflow.draft!, node, previousNodeId);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.enabled = resp.enabled;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
publish: async () => {
ensureInitialized();
duplicateNode: async (node: WorkflowNode) => {
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,
});
const root = duplicateNode(get().workflow.draft!, node);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.trigger = resp.trigger;
draft.triggerCron = resp.triggerCron;
draft.content = resp.content;
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
rollback: async () => {
ensureInitialized();
updateNode: async (node: WorkflowNode) => {
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!,
draft: root,
hasDraft: false,
trigger: startConfig.trigger,
triggerCron: startConfig.triggerCron,
});
const root = updateNode(get().workflow.draft!, node);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.trigger = resp.trigger;
draft.triggerCron = resp.triggerCron;
draft.content = resp.content;
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
addNode: async (node, previousNodeId) => {
ensureInitialized();
removeNode: async (node: WorkflowNode) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = addNode(get().workflow.draft!, node, previousNodeId);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
const root = removeNode(get().workflow.draft!, node.id);
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;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
duplicateNode: async (node) => {
ensureInitialized();
addBranch: async (branchId: string) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = duplicateNode(get().workflow.draft!, node);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
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;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
updateNode: async (node) => {
ensureInitialized();
duplicateBranch: async (branchId: string, index: number) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = updateNode(get().workflow.draft!, node);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
const root = duplicateBranch(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;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
removeNode: async (node) => {
ensureInitialized();
removeBranch: async (branchId: string, index: number) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = removeNode(get().workflow.draft!, node.id);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
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;
}),
};
});
},
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
addBranch: async (branchId) => {
ensureInitialized();
getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => {
return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, typeFilter);
},
}));
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;
}),
};
});
},
duplicateBranch: async (branchId, index) => {
ensureInitialized();
const root = duplicateBranch(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;
}),
};
});
},
removeBranch: async (branchId, index) => {
ensureInitialized();
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;
}),
};
});
},
getWorkflowOuptutBeforeId: (nodeId, typeFilter) => {
return getOutputBeforeNodeId(get().workflow.draft!, nodeId, typeFilter);
},
};
});
+28
View File
@@ -0,0 +1,28 @@
import { type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
export interface WorkflowState {
workflow: WorkflowModel;
initialized: boolean;
}
export interface WorkflowActions {
init(id: string): void;
destroy(): void;
setBaseInfo: (name: string, description: string) => void;
setEnabled(enabled: boolean): void;
publish(): void;
rollback(): void;
addNode: (node: WorkflowNode, previousNodeId: string) => void;
duplicateNode: (node: WorkflowNode) => void;
updateNode: (node: WorkflowNode) => void;
removeNode: (node: WorkflowNode) => void;
addBranch: (branchId: string) => void;
duplicateBranch: (branchId: string, index: number) => void;
removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[];
}
export interface WorkflowStore extends WorkflowState, WorkflowActions {}