From 396be6f9d58a43370bbe1fba9717f5a31e23eaab Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 29 Jul 2025 14:37:06 +0800 Subject: [PATCH] refactor(ui): optimize stores --- ui/src/i18n/locales/en/nls.workflow.json | 10 +- ui/src/i18n/locales/zh/nls.workflow.json | 12 +- ui/src/index.css | 2 +- ui/src/pages/settings/Settings.tsx | 6 +- ui/src/pages/workflows/WorkflowDetail.tsx | 117 +++-- .../pages/workflows/WorkflowDetailDesign.tsx | 111 +--- ui/src/stores/access/index.ts | 13 +- ui/src/stores/access/types.ts | 16 + ui/src/stores/contact/index.ts | 13 +- ui/src/stores/contact/types.ts | 14 + ui/src/stores/workflow/index.ts | 480 +++++++++--------- ui/src/stores/workflow/types.ts | 28 + 12 files changed, 423 insertions(+), 399 deletions(-) create mode 100644 ui/src/stores/access/types.ts create mode 100644 ui/src/stores/contact/types.ts create mode 100644 ui/src/stores/workflow/types.ts diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 39acce5bf..b7ce41366 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -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" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index be0c0203f..178379aa2 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -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": "执行历史" } diff --git a/ui/src/index.css b/ui/src/index.css index f8fbe4238..b37b38024 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -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)); } diff --git a/ui/src/pages/settings/Settings.tsx b/ui/src/pages/settings/Settings.tsx index 6de4be1ed..9386e19f4 100644 --- a/ui/src/pages/settings/Settings.tsx +++ b/ui/src/pages/settings/Settings.tsx @@ -16,7 +16,7 @@ const Settings = () => { ["ssl-provider", "settings.sslprovider.tab", ], ["persistence", "settings.persistence.tab", ], ] satisfies [string, string, React.ReactElement][]; - const [menuKey, setMenuValue] = useState(); + const [menuKey, setMenuKey] = useState(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}`); }; diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 80db33758..921ec8a0f 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -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(null); + const divHeaderSize = useSize(divHeaderRef); + const tabs = [ ["design", "workflow.detail.design.tab", ], ["runs", "workflow.detail.runs.tab", ], ] satisfies [string, string, React.ReactElement][]; - const [tabValue, setTabValue] = useState(); + const [tabValue, setTabValue] = useState(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 (
-
-
- - +
+
+
+ + -
- ({ - value: key, - label: {t(label)}, - icon: ( - - {icon} - - ), - }))} - size="large" - value={tabValue} - defaultValue="design" - onChange={handleTabChange} - /> +
+ ({ + value: key, + label: {t(label)}, + icon: ( + + {icon} + + ), + }))} + size="large" + value={tabValue} + onChange={handleTabChange} + /> +
+
+
+ + +
-
+
{ return (

@@ -158,6 +213,8 @@ const WorkflowDetailBaseName = () => { { 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 = () => { handleValueConfirm(e.target.value)} diff --git a/ui/src/pages/workflows/WorkflowDetailDesign.tsx b/ui/src/pages/workflows/WorkflowDetailDesign.tsx index 5bd694e74..dd64fec48 100644 --- a/ui/src/pages/workflows/WorkflowDetailDesign.tsx +++ b/ui/src/pages/workflows/WorkflowDetailDesign.tsx @@ -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> | 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> | 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 (
{ }, }} > -
+
@@ -156,32 +94,27 @@ const WorkflowDetailDesign = () => {
- - - - - , - onClick: handleRollbackClick, - }, - ], - }} - trigger={["click"]} - > -
diff --git a/ui/src/stores/access/index.ts b/ui/src/stores/access/index.ts index 207fc1fea..c51fa3bf7 100644 --- a/ui/src/stores/access/index.ts +++ b/ui/src/stores/access/index.ts @@ -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; - createAccess: (access: MaybeModelRecord) => Promise; - updateAccess: (access: MaybeModelRecordWithId) => Promise; - deleteAccess: (access: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => Promise; -} - -export const useAccessesStore = create((set) => { +export const useAccessesStore = create((set) => { let fetcher: Promise | null = null; // 防止多次重复请求 return { diff --git a/ui/src/stores/access/types.ts b/ui/src/stores/access/types.ts new file mode 100644 index 000000000..2c987beb6 --- /dev/null +++ b/ui/src/stores/access/types.ts @@ -0,0 +1,16 @@ +import { type AccessModel } from "@/domain/access"; + +export interface AccessesState { + accesses: AccessModel[]; + loading: boolean; + loadedAtOnce: boolean; +} + +export interface AccessesActions { + fetchAccesses: () => Promise; + createAccess: (access: MaybeModelRecord) => Promise; + updateAccess: (access: MaybeModelRecordWithId) => Promise; + deleteAccess: (access: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => Promise; +} + +export interface AccessesStore extends AccessesState, AccessesActions {} diff --git a/ui/src/stores/contact/index.ts b/ui/src/stores/contact/index.ts index ae8e25d20..e554bfb8b 100644 --- a/ui/src/stores/contact/index.ts +++ b/ui/src/stores/contact/index.ts @@ -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; - setEmails: (emails: string[]) => Promise; - addEmail: (email: string) => Promise; - removeEmail: (email: string) => Promise; -} - -export const useContactEmailsStore = create((set, get) => { +export const useContactEmailsStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let settings: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 diff --git a/ui/src/stores/contact/types.ts b/ui/src/stores/contact/types.ts new file mode 100644 index 000000000..a1ee603ce --- /dev/null +++ b/ui/src/stores/contact/types.ts @@ -0,0 +1,14 @@ +export interface ContactEmailsState { + emails: string[]; + loading: boolean; + loadedAtOnce: boolean; +} + +export interface ContactEmailsActions { + fetchEmails: () => Promise; + setEmails: (emails: string[]) => Promise; + addEmail: (email: string) => Promise; + removeEmail: (email: string) => Promise; +} + +export interface ContactEmailsStore extends ContactEmailsState, ContactEmailsActions {} diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 405a5bb20..962ec6a97 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -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((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((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); + }, + }; +}); diff --git a/ui/src/stores/workflow/types.ts b/ui/src/stores/workflow/types.ts new file mode 100644 index 000000000..da85e6f1b --- /dev/null +++ b/ui/src/stores/workflow/types.ts @@ -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 {}