mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
refactor(ui): optimize stores
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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>; // 记录当前设置的其他字段,保存回数据库时用
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user