660 lines
22 KiB
TypeScript
660 lines
22 KiB
TypeScript
// List slice: schedule list, artifact list, current schedule, loading/busy flags,
|
|
// and most mutation/loader actions. Filter inputs (scheduleKeyword/artifactKeyword)
|
|
// live in canvasSlice since they bind to the left-panel UI. Cross-slice actions
|
|
// live here when their primary write is to LIST fields; they reach into
|
|
// CANVAS / DIALOG / RUNS via `get()`.
|
|
|
|
import { ApiRequestError, type Schedule, type ScheduleArtifact, type ScheduleEdge, type ScheduleNode } from "../../../services/api";
|
|
import type { StateCreator } from "zustand";
|
|
|
|
import {
|
|
applyPositionDrafts,
|
|
applyServerUpdatedSchedule,
|
|
artifactNodeKey,
|
|
notify,
|
|
parseObject,
|
|
requireApi,
|
|
setApiOnline,
|
|
scheduleToForm,
|
|
withMutation,
|
|
} from "./helpers";
|
|
import {
|
|
PYTHON_VERSION_OPTIONS,
|
|
type NodeForm,
|
|
type ScheduleForm,
|
|
} from "./types";
|
|
import type { SchedulesStore } from "./useSchedulesStore";
|
|
|
|
// ---- State ----
|
|
|
|
export type ListSliceState = {
|
|
schedules: Schedule[];
|
|
artifacts: ScheduleArtifact[];
|
|
schedule: Schedule | null;
|
|
loading: boolean;
|
|
busy: string | null;
|
|
};
|
|
|
|
// ---- Actions ----
|
|
|
|
export type ListSliceActions = {
|
|
// pure setters
|
|
setSchedule: (
|
|
value: Schedule | null | ((current: Schedule | null) => Schedule | null),
|
|
) => void;
|
|
setSchedules: (
|
|
value: Schedule[] | ((current: Schedule[]) => Schedule[]),
|
|
) => void;
|
|
setArtifacts: (
|
|
value: ScheduleArtifact[]
|
|
| ((current: ScheduleArtifact[]) => ScheduleArtifact[]),
|
|
) => void;
|
|
setLoading: (loading: boolean) => void;
|
|
setBusy: (busy: string | null) => void;
|
|
|
|
// data loaders
|
|
loadInitial: () => Promise<void>;
|
|
refreshLists: (preferredScheduleId?: string | null) => Promise<void>;
|
|
chooseSchedule: (scheduleId: string) => Promise<void>;
|
|
|
|
// mutations
|
|
addSchedule: (name: string) => Promise<void>;
|
|
removeSchedule: (target?: Schedule) => Promise<void>;
|
|
renameSchedule: (target: Schedule) => Promise<void>;
|
|
removeArtifact: (artifact: ScheduleArtifact) => Promise<void>;
|
|
saveSchedule: () => Promise<void>;
|
|
runNow: () => Promise<void>;
|
|
addArtifactAt: (
|
|
artifact: ScheduleArtifact,
|
|
positionX: number,
|
|
positionY: number,
|
|
) => Promise<void>;
|
|
saveNode: (selectedNode: ScheduleNode | null) => Promise<void>;
|
|
removeNode: (target?: ScheduleNode) => Promise<void>;
|
|
removeEdge: (target?: ScheduleEdge) => Promise<void>;
|
|
checkDag: () => Promise<void>;
|
|
connectTo: (targetNodeId: string) => Promise<void>;
|
|
};
|
|
|
|
// `SchedulesStore` is the combined state+actions of all slices, so `get()`
|
|
// returns a fully typed snapshot — no `any` propagation.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceState & ListSliceActions> = (set, get) => {
|
|
// ---- initial state ----
|
|
const state: ListSliceState = {
|
|
schedules: [],
|
|
artifacts: [],
|
|
schedule: null,
|
|
loading: true,
|
|
busy: null,
|
|
};
|
|
|
|
return {
|
|
...state,
|
|
|
|
// ---- pure setters ----
|
|
|
|
setSchedule: (value) =>
|
|
set((s: any) => ({
|
|
schedule:
|
|
typeof value === "function"
|
|
? (value as (current: Schedule | null) => Schedule | null)(s.schedule)
|
|
: value,
|
|
})),
|
|
setSchedules: (value) =>
|
|
set((s: any) => ({
|
|
schedules:
|
|
typeof value === "function"
|
|
? (value as (current: Schedule[]) => Schedule[])(s.schedules)
|
|
: value,
|
|
})),
|
|
setArtifacts: (value) =>
|
|
set((s: any) => ({
|
|
artifacts:
|
|
typeof value === "function"
|
|
? (value as (current: ScheduleArtifact[]) => ScheduleArtifact[])(
|
|
s.artifacts,
|
|
)
|
|
: value,
|
|
})),
|
|
setLoading: (loading) => set({ loading }),
|
|
setBusy: (busy) => set({ busy }),
|
|
|
|
// ---- data loaders ----
|
|
|
|
loadInitial: async () => {
|
|
const api = requireApi();
|
|
let cancelled = false;
|
|
set({ loading: true });
|
|
try {
|
|
const [scheduleItems, artifactItems] = await Promise.all([
|
|
api.listSchedules(),
|
|
api.listScheduleArtifacts(),
|
|
]);
|
|
if (cancelled) return;
|
|
set({ schedules: scheduleItems, artifacts: artifactItems });
|
|
if (scheduleItems[0]) {
|
|
const detail = await api.getSchedule(scheduleItems[0].schedule_id);
|
|
if (!cancelled) {
|
|
set({ schedule: applyPositionDrafts(detail) });
|
|
}
|
|
}
|
|
setApiOnline(true);
|
|
} catch (error) {
|
|
if (cancelled) return;
|
|
setApiOnline(false);
|
|
notify({
|
|
tone: "error",
|
|
message: error instanceof Error ? error.message : "调度数据加载失败",
|
|
});
|
|
} finally {
|
|
if (!cancelled) set({ loading: false });
|
|
}
|
|
},
|
|
|
|
refreshLists: async (preferredScheduleId) => {
|
|
const api = requireApi();
|
|
const [scheduleItems, artifactItems] = await Promise.all([
|
|
api.listSchedules(),
|
|
api.listScheduleArtifacts(),
|
|
]);
|
|
set({ schedules: scheduleItems, artifacts: artifactItems });
|
|
const targetId =
|
|
preferredScheduleId
|
|
?? get().schedule?.schedule_id
|
|
?? scheduleItems[0]?.schedule_id
|
|
?? null;
|
|
if (!targetId) {
|
|
set({ schedule: null });
|
|
return;
|
|
}
|
|
const detail = await api.getSchedule(targetId);
|
|
set({ schedule: applyPositionDrafts(detail) });
|
|
},
|
|
|
|
chooseSchedule: async (scheduleId) => {
|
|
const api = requireApi();
|
|
const current = get().schedule;
|
|
if (scheduleId === current?.schedule_id || get().busy) return;
|
|
set({ busy: "load-schedule" });
|
|
get().clearAllPositionDrafts();
|
|
set({
|
|
selectedNodeId: null,
|
|
selectedEdgeId: null,
|
|
linkSourceId: null,
|
|
cronResult: null,
|
|
runs: [],
|
|
runsLoading: false,
|
|
});
|
|
try {
|
|
set({ schedule: await api.getSchedule(scheduleId) });
|
|
setApiOnline(true);
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "调度详情加载失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
// ---- mutations ----
|
|
|
|
addSchedule: async (name) => {
|
|
const api = requireApi();
|
|
const scheduleName = name.trim();
|
|
if (get().busy || !scheduleName) return;
|
|
set({ busy: "create-schedule" });
|
|
try {
|
|
const created = await api.createSchedule({
|
|
schedule_name: scheduleName,
|
|
description: "在画布中拖入稳定版本并配置执行顺序",
|
|
trigger_type: "manual",
|
|
timezone: "Asia/Shanghai",
|
|
enabled: false,
|
|
});
|
|
set((s: any) => ({
|
|
schedules: [created, ...s.schedules],
|
|
schedule: created,
|
|
createDialogOpen: false,
|
|
newScheduleName: "",
|
|
}));
|
|
get().clearAllPositionDrafts();
|
|
set({ selectedNodeId: null, selectedEdgeId: null });
|
|
notify({ tone: "success", message: "调度方案已创建" });
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "创建调度失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
removeSchedule: async (target) => {
|
|
const api = requireApi();
|
|
const target_ = target ?? get().schedule;
|
|
if (!target_ || get().busy) return;
|
|
set({ contextMenu: null });
|
|
if (!window.confirm(`确定删除调度"${target_.schedule_name}"吗?`)) return;
|
|
set({ busy: "delete-schedule" });
|
|
try {
|
|
await api.deleteSchedule(target_.schedule_id, target_.workflow_version);
|
|
const remaining = get().schedules.filter(
|
|
(item) => item.schedule_id !== target_.schedule_id,
|
|
);
|
|
set({ schedules: remaining });
|
|
if (get().schedule?.schedule_id === target_.schedule_id) {
|
|
get().clearAllPositionDrafts();
|
|
set({
|
|
schedule: null,
|
|
selectedNodeId: null,
|
|
selectedEdgeId: null,
|
|
});
|
|
if (remaining[0]) {
|
|
const detail = await api.getSchedule(remaining[0].schedule_id);
|
|
set({ schedule: detail });
|
|
}
|
|
}
|
|
notify({ tone: "success", message: "调度方案已删除" });
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "删除调度失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
renameSchedule: async (target) => {
|
|
const api = requireApi();
|
|
if (get().busy) return;
|
|
set({ contextMenu: null });
|
|
const scheduleName = window
|
|
.prompt("请输入新的调度方案名称", target.schedule_name)
|
|
?.trim();
|
|
if (!scheduleName || scheduleName === target.schedule_name) return;
|
|
set({ busy: "rename-schedule" });
|
|
try {
|
|
const updated = await api.updateSchedule(target.schedule_id, {
|
|
workflow_version: target.workflow_version,
|
|
schedule_name: scheduleName,
|
|
});
|
|
set((s: any) => ({
|
|
schedules: s.schedules.map((item: Schedule) =>
|
|
item.schedule_id === updated.schedule_id
|
|
? { ...updated, nodes: [], edges: [] }
|
|
: item
|
|
),
|
|
schedule:
|
|
s.schedule?.schedule_id === updated.schedule_id
|
|
? updated
|
|
: s.schedule,
|
|
}));
|
|
notify({ tone: "success", message: "调度方案已改名" });
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "调度方案改名失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
removeArtifact: async (artifact) => {
|
|
const api = requireApi();
|
|
if (get().busy) return;
|
|
set({ contextMenu: null });
|
|
if (
|
|
!window.confirm(
|
|
`确定将"${artifact.script_name} ${artifact.version_label}"移出调度列表吗?\n`
|
|
+ "稳定版本本身和历史运行记录不会被删除。",
|
|
)
|
|
) return;
|
|
set({ busy: "delete-artifact" });
|
|
try {
|
|
await api.hideScheduleArtifact(artifact.versions_id);
|
|
set((s: any) => ({
|
|
artifacts: s.artifacts.filter(
|
|
(item: ScheduleArtifact) => item.versions_id !== artifact.versions_id,
|
|
),
|
|
}));
|
|
notify({
|
|
tone: "success",
|
|
message: "已移出调度列表,稳定版本和历史记录保持不变",
|
|
});
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "移出调度列表失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
saveSchedule: async () => {
|
|
const api = requireApi();
|
|
const state_ = get();
|
|
const { schedule, scheduleForm } = state_;
|
|
if (!schedule || state_.busy) return;
|
|
const scheduleFormTyped = scheduleForm as ScheduleForm;
|
|
const maxConcurrency = Number(scheduleFormTyped.maxConcurrency);
|
|
if (!scheduleFormTyped.scheduleName.trim()) {
|
|
notify({ tone: "error", message: "调度名称不能为空" });
|
|
return;
|
|
}
|
|
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
notify({ tone: "error", message: "最大并发数必须是正整数" });
|
|
return;
|
|
}
|
|
set({ busy: "save-schedule" });
|
|
let updated: Schedule = schedule;
|
|
try {
|
|
const { positionDrafts } = await import("./helpers");
|
|
for (const [nodeId, position] of positionDrafts.entries()) {
|
|
updated = await api.updateScheduleNode(updated.schedule_id, nodeId, {
|
|
workflow_version: updated.workflow_version,
|
|
position_x: position.position_x,
|
|
position_y: position.position_y,
|
|
});
|
|
}
|
|
updated = await api.updateSchedule(updated.schedule_id, {
|
|
workflow_version: updated.workflow_version,
|
|
schedule_name: scheduleFormTyped.scheduleName.trim(),
|
|
description: scheduleFormTyped.description.trim() || null,
|
|
trigger_type: scheduleFormTyped.triggerType,
|
|
cron_expression:
|
|
scheduleFormTyped.triggerType === "cron"
|
|
? scheduleFormTyped.cronExpression.trim()
|
|
: null,
|
|
timezone: scheduleFormTyped.timezone.trim(),
|
|
enabled: scheduleFormTyped.enabled,
|
|
max_concurrency: maxConcurrency,
|
|
failure_policy: scheduleFormTyped.failurePolicy,
|
|
});
|
|
get().clearAllPositionDrafts();
|
|
set((s: any) => ({
|
|
schedule: updated,
|
|
schedules: s.schedules.map((item: Schedule) =>
|
|
item.schedule_id === updated.schedule_id
|
|
? { ...updated, nodes: [], edges: [] }
|
|
: item
|
|
),
|
|
}));
|
|
|
|
notify({ tone: "success", message: "调度配置已保存" });
|
|
} catch (error) {
|
|
const localDraft = applyPositionDrafts(updated);
|
|
set({ schedule: localDraft });
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "调度配置保存失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
runNow: async () => {
|
|
const api = requireApi();
|
|
const state_ = get();
|
|
const { schedule, positionDraftCount } = state_;
|
|
if (!schedule || state_.busy) return;
|
|
if (positionDraftCount > 0) {
|
|
notify({
|
|
tone: "info",
|
|
message: "还有节点位置未保存,请先点击'保存配置'再运行",
|
|
});
|
|
return;
|
|
}
|
|
if (!schedule.dag_validation.valid || schedule.nodes.length === 0) {
|
|
notify({
|
|
tone: "error",
|
|
message: "当前调度必须包含有效的非空 DAG 才能运行",
|
|
});
|
|
return;
|
|
}
|
|
set({ busy: "run-now" });
|
|
try {
|
|
const created = await api.runScheduleNow(schedule.schedule_id);
|
|
set((s: any) => ({
|
|
runs: [
|
|
created,
|
|
...s.runs.filter((item: { run_id: string }) => item.run_id !== created.run_id),
|
|
].slice(0, 20),
|
|
schedule:
|
|
s.schedule
|
|
? { ...s.schedule, last_run_at: created.queued_at }
|
|
: s.schedule,
|
|
schedules: s.schedules.map((item: Schedule) =>
|
|
item.schedule_id === schedule.schedule_id
|
|
? { ...item, last_run_at: created.queued_at }
|
|
: item
|
|
),
|
|
}));
|
|
notify({
|
|
tone: "success",
|
|
message: `运行 ${created.run_id.slice(-8)} 已进入队列`,
|
|
});
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "立即运行失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
addArtifactAt: async (artifact, positionX, positionY) => {
|
|
const state_ = get();
|
|
const { schedule } = state_;
|
|
if (!schedule) {
|
|
notify({ tone: "info", message: "请先新建或选择一个调度方案" });
|
|
return;
|
|
}
|
|
const nodeKey = artifactNodeKey(artifact, schedule);
|
|
const updated = await withMutation(
|
|
set,
|
|
get,
|
|
"add-node",
|
|
() => requireApi().createScheduleNode(schedule.schedule_id, {
|
|
workflow_version: schedule.workflow_version,
|
|
node_key: nodeKey,
|
|
node_name: artifact.script_name,
|
|
versions_id: artifact.versions_id,
|
|
timeout_seconds: 600,
|
|
retry_count: 0,
|
|
retry_interval_sec: 5,
|
|
position_x: Math.max(20, Math.round(positionX)),
|
|
position_y: Math.max(20, Math.round(positionY)),
|
|
arguments_json: {},
|
|
env_refs_json: {},
|
|
python_version: "3.12",
|
|
}),
|
|
`${artifact.script_name} 已加入画布`,
|
|
);
|
|
if (updated) {
|
|
const created = updated.nodes.find((item) => item.node_key === nodeKey);
|
|
set({ selectedNodeId: created?.node_id ?? null });
|
|
}
|
|
},
|
|
|
|
saveNode: async (selectedNode) => {
|
|
const api = requireApi();
|
|
const state_ = get();
|
|
const { schedule, nodeForm } = state_;
|
|
if (!schedule || !selectedNode) return;
|
|
try {
|
|
const nodeFormTyped = nodeForm as NodeForm;
|
|
const timeoutSeconds = Number(nodeFormTyped.timeoutSeconds);
|
|
const retryCount = Number(nodeFormTyped.retryCount);
|
|
const retryIntervalSec = Number(nodeFormTyped.retryIntervalSec);
|
|
if (!nodeFormTyped.nodeName.trim()) throw new Error("节点名称不能为空");
|
|
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1) {
|
|
throw new Error("超时时间必须是正整数");
|
|
}
|
|
if (!Number.isInteger(retryCount) || retryCount < 0) {
|
|
throw new Error("重试次数必须是非负整数");
|
|
}
|
|
if (!Number.isInteger(retryIntervalSec) || retryIntervalSec < 0) {
|
|
throw new Error("重试间隔必须是非负整数");
|
|
}
|
|
if (!PYTHON_VERSION_OPTIONS.includes(nodeFormTyped.pythonVersion)) {
|
|
throw new Error("Python 版本必须是 3.8 / 3.10 / 3.12");
|
|
}
|
|
const argumentsJson = parseObject(nodeFormTyped.argumentsJson, "运行参数");
|
|
const rawEnv = parseObject(nodeFormTyped.envRefsJson, "环境引用");
|
|
const envRefsJson = Object.fromEntries(
|
|
Object.entries(rawEnv).map(([key, value]) => {
|
|
if (typeof value !== "string") {
|
|
throw new Error("环境引用的值必须是字符串");
|
|
}
|
|
return [key, value];
|
|
}),
|
|
);
|
|
await withMutation(
|
|
set,
|
|
get,
|
|
"save-node",
|
|
() =>
|
|
api.updateScheduleNode(
|
|
schedule.schedule_id,
|
|
selectedNode.node_id,
|
|
{
|
|
workflow_version: schedule.workflow_version,
|
|
node_name: nodeFormTyped.nodeName.trim(),
|
|
timeout_seconds: timeoutSeconds,
|
|
retry_count: retryCount,
|
|
retry_interval_sec: retryIntervalSec,
|
|
arguments_json: argumentsJson,
|
|
env_refs_json: envRefsJson,
|
|
python_version: nodeFormTyped.pythonVersion,
|
|
},
|
|
),
|
|
"节点配置已保存",
|
|
);
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "节点配置保存失败");
|
|
}
|
|
},
|
|
|
|
removeNode: async (target) => {
|
|
const api = requireApi();
|
|
const state_ = get();
|
|
const { schedule, selectedNodeId } = state_;
|
|
const node = target
|
|
?? schedule?.nodes.find((item) => item.node_id === selectedNodeId)
|
|
?? null;
|
|
if (!schedule || !node || state_.busy) return;
|
|
set({ contextMenu: null });
|
|
if (!window.confirm(`确定删除节点"${node.node_name}"吗?`)) return;
|
|
set({ busy: "delete-node" });
|
|
try {
|
|
let updated: Schedule;
|
|
try {
|
|
// 先走普通删除:没有历史记录时不额外打扰用户。
|
|
updated = await api.deleteScheduleNode(
|
|
schedule.schedule_id,
|
|
node.node_id,
|
|
schedule.workflow_version,
|
|
);
|
|
} catch (error) {
|
|
const requiresHistoryConfirmation =
|
|
error instanceof ApiRequestError
|
|
&& error.status === 409
|
|
&& error.code === "node_execution_history_exists";
|
|
if (!requiresHistoryConfirmation) throw error;
|
|
if (!window.confirm("该节点有运行日志,是否一并删除?")) return;
|
|
updated = await api.deleteScheduleNode(
|
|
schedule.schedule_id,
|
|
node.node_id,
|
|
schedule.workflow_version,
|
|
{ delete_execution_history: true },
|
|
);
|
|
}
|
|
applyServerUpdatedSchedule(set, updated);
|
|
if (selectedNodeId === node.node_id) set({ selectedNodeId: null });
|
|
notify({ tone: "success", message: "节点及其运行日志已删除" });
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "删除节点失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
removeEdge: async (target) => {
|
|
const api = requireApi();
|
|
const state_ = get();
|
|
const { schedule, selectedEdgeId } = state_;
|
|
const edge = target
|
|
?? schedule?.edges.find((item) => item.edge_id === selectedEdgeId)
|
|
?? null;
|
|
if (!schedule || !edge || state_.busy) return;
|
|
set({ contextMenu: null });
|
|
const updated = await withMutation(
|
|
set,
|
|
get,
|
|
"delete-edge",
|
|
() =>
|
|
api.deleteScheduleEdge(
|
|
schedule.schedule_id,
|
|
edge.edge_id,
|
|
schedule.workflow_version,
|
|
),
|
|
"连线已删除",
|
|
);
|
|
if (updated && selectedEdgeId === edge.edge_id) {
|
|
set({ selectedEdgeId: null });
|
|
}
|
|
},
|
|
|
|
checkDag: async () => {
|
|
const api = requireApi();
|
|
const { schedule } = get();
|
|
if (!schedule || get().busy) return;
|
|
set({ busy: "validate" });
|
|
try {
|
|
const result = await api.validateSchedule(schedule.schedule_id);
|
|
set((s: any) => ({
|
|
schedule: s.schedule
|
|
? { ...s.schedule, dag_validation: result }
|
|
: s.schedule,
|
|
}));
|
|
notify({
|
|
tone: result.valid ? "success" : "error",
|
|
message: result.valid
|
|
? "当前 DAG 通过校验"
|
|
: "当前 DAG 存在校验问题",
|
|
});
|
|
} catch (error) {
|
|
const { handleError } = await import("./helpers");
|
|
await handleError(get, error, "DAG 校验失败");
|
|
} finally {
|
|
set({ busy: null });
|
|
}
|
|
},
|
|
|
|
connectTo: async (targetNodeId) => {
|
|
const state_ = get();
|
|
const { schedule, linkSourceId } = state_;
|
|
if (!schedule || !linkSourceId || state_.busy) return;
|
|
if (linkSourceId === targetNodeId) {
|
|
set({ linkSourceId: null });
|
|
notify({ tone: "info", message: "已取消连线" });
|
|
return;
|
|
}
|
|
const sourceId = linkSourceId;
|
|
set({ linkSourceId: null });
|
|
await withMutation(
|
|
set,
|
|
get,
|
|
"create-edge",
|
|
() =>
|
|
requireApi().createScheduleEdge(schedule.schedule_id, {
|
|
workflow_version: schedule.workflow_version,
|
|
source_node_id: sourceId,
|
|
target_node_id: targetNodeId,
|
|
}),
|
|
"节点连线已创建",
|
|
);
|
|
},
|
|
};
|
|
};
|
|
|
|
// (avoid unused-import warning for scheduleToForm — referenced for parity with original store)
|
|
void scheduleToForm;
|