update:原生确认框替换为自定义弹窗
This commit is contained in:
@@ -39,7 +39,7 @@ function ConfirmDialog({
|
||||
}: ConfirmDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="rounded-[11px] border-[#dce4eb] bg-white">
|
||||
<AlertDialogContent className="rounded-[11px] border border-[#dce4eb] bg-white ring-0">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-[#1c2d42]">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-[#56687c]">
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
import { WelcomePanel } from "./WelcomePanel";
|
||||
import { PublishModal } from "./PublishModal";
|
||||
import { ScriptExplorer } from "./ScriptExplorer";
|
||||
import { ScriptsPendingConfirmDialog } from "./ScriptsPendingConfirm";
|
||||
import { TreeContextMenu } from "./TreeContextMenu";
|
||||
import { VersionReceiptModal } from "./VersionReceiptModal";
|
||||
|
||||
import { useScriptsPendingConfirm } from "./useScriptsPendingConfirm";
|
||||
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
||||
import { useUiStore } from "./state/uiStore";
|
||||
import { toast } from "sonner";
|
||||
@@ -75,6 +76,18 @@ export default function ScriptsPage() {
|
||||
const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish);
|
||||
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
|
||||
|
||||
const {
|
||||
pendingConfirm,
|
||||
setPendingConfirm,
|
||||
requestCloseTab,
|
||||
handleConfirm,
|
||||
} = useScriptsPendingConfirm(scripts, {
|
||||
deleteScript,
|
||||
deleteDataResource,
|
||||
deleteDirectory,
|
||||
closeTab,
|
||||
});
|
||||
|
||||
// ui store
|
||||
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||
if (notice.tone === "error") toast.error(notice.message);
|
||||
@@ -206,27 +219,11 @@ export default function ScriptsPage() {
|
||||
};
|
||||
}, [contextMenu, closeContextMenu]);
|
||||
|
||||
// python editor handlers with scriptId forwarding
|
||||
const handleSetPythonEditorContent = (scriptId: string, value: string) => {
|
||||
setPythonEditorContent(scriptId, value);
|
||||
};
|
||||
const handleSavePythonEditor = (scriptId: string) => {
|
||||
void savePythonEditor(scriptId);
|
||||
};
|
||||
const handleExitPythonEditor = (scriptId: string) => {
|
||||
exitPythonEditor(scriptId);
|
||||
};
|
||||
const handleClosePythonTab = (scriptId: string) => {
|
||||
void closeTab(scriptId);
|
||||
};
|
||||
|
||||
// 5) handlers
|
||||
// 5) handlers
|
||||
const SCRIPT_EXTS = [".py", ".ipynb"];
|
||||
const DATA_EXTS = [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet", ".txt"];
|
||||
|
||||
const matchesExt = (name: string, exts: string[]) =>
|
||||
exts.some((ext) => name.toLowerCase().endsWith(ext));
|
||||
|
||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files ?? []);
|
||||
event.target.value = "";
|
||||
@@ -381,17 +378,17 @@ export default function ScriptsPage() {
|
||||
void endEditing();
|
||||
}
|
||||
}}
|
||||
onClose={(scriptId, event) => void closeTab(scriptId, event)}
|
||||
onClose={(scriptId, event) => void requestCloseTab(scriptId, event)}
|
||||
onSwitchTab={switchTab}
|
||||
onNewTab={() => openCreateDialog("")}
|
||||
onPublish={() => openPublishDialog(selected)}
|
||||
scripts={scripts}
|
||||
pythonEditorBuffers={pythonEditorBuffers}
|
||||
onOpenPythonEditor={() => void openPythonEditor(selected)}
|
||||
onSetPythonEditorContent={handleSetPythonEditorContent}
|
||||
onSavePythonEditor={handleSavePythonEditor}
|
||||
onExitPythonEditor={handleExitPythonEditor}
|
||||
onClosePythonTab={handleClosePythonTab}
|
||||
onSetPythonEditorContent={setPythonEditorContent}
|
||||
onSavePythonEditor={(scriptId) => void savePythonEditor(scriptId)}
|
||||
onExitPythonEditor={exitPythonEditor}
|
||||
onClosePythonTab={(scriptId) => void requestCloseTab(scriptId)}
|
||||
onInfo={(t) => pushToast(t)}
|
||||
/>
|
||||
) : (
|
||||
@@ -429,19 +426,35 @@ export default function ScriptsPage() {
|
||||
selectScript(scriptId);
|
||||
closeContextMenu();
|
||||
}}
|
||||
onRemoveScript={(s) => void deleteScript(s)}
|
||||
onRemoveScript={(s) => setPendingConfirm({ kind: "script", script: s })}
|
||||
onToggleLock={(s) => void toggleScriptLock(s)}
|
||||
onOpenCreateDialog={(parentPath, scriptType) =>
|
||||
openCreateDialog(parentPath, scriptType)}
|
||||
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
|
||||
onChooseUpload={(parentPath) => triggerUpload(parentPath ?? "")}
|
||||
onRemoveDirectory={(p) => void deleteDirectory(p)}
|
||||
onRemoveDirectory={(p) =>
|
||||
setPendingConfirm({ kind: "directory", path: p })}
|
||||
onCopyResourcePath={(p) => void handleCopyResourcePath(p)}
|
||||
onPreviewResource={openResourcePreview}
|
||||
onRemoveResource={(id) => void deleteDataResource(id)}
|
||||
onRemoveResource={(id) => {
|
||||
const resource = dataResources.find((item) => item.resource_id === id);
|
||||
setPendingConfirm({
|
||||
kind: "resource",
|
||||
id,
|
||||
name: resource?.resource_name ?? id,
|
||||
});
|
||||
}}
|
||||
onClose={closeContextMenu}
|
||||
/>
|
||||
|
||||
<ScriptsPendingConfirmDialog
|
||||
pending={pendingConfirm}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPendingConfirm(null);
|
||||
}}
|
||||
onConfirm={() => void handleConfirm()}
|
||||
/>
|
||||
|
||||
<DataResourcePreviewDialog
|
||||
target={resourcePreview}
|
||||
onClose={() => setResourcePreview(null)}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||
import type { ScriptItem } from "~/services/api";
|
||||
|
||||
export type ScriptsPendingConfirm =
|
||||
| { kind: "script"; script: ScriptItem }
|
||||
| { kind: "resource"; id: string; name: string }
|
||||
| { kind: "directory"; path: string }
|
||||
| { kind: "close-tab"; id: string; name: string };
|
||||
|
||||
type ScriptsPendingConfirmDialogProps = {
|
||||
pending: ScriptsPendingConfirm | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function copyFor(pending: ScriptsPendingConfirm) {
|
||||
switch (pending.kind) {
|
||||
case "script":
|
||||
return {
|
||||
title: "确定删除文件?",
|
||||
description: `确定删除文件"${pending.script.script_name}"吗?稳定版本会保留。`,
|
||||
confirmLabel: "删除",
|
||||
destructive: true,
|
||||
};
|
||||
case "resource":
|
||||
return {
|
||||
title: "确定删除数据资源?",
|
||||
description: `确定删除数据资源"${pending.name}"吗?稳定版本会保留。`,
|
||||
confirmLabel: "删除",
|
||||
destructive: true,
|
||||
};
|
||||
case "directory":
|
||||
return {
|
||||
title: "确定删除文件夹?",
|
||||
description: `确定递归删除文件夹"${pending.path}"及其内容吗?稳定版本会保留。`,
|
||||
confirmLabel: "删除",
|
||||
destructive: true,
|
||||
};
|
||||
case "close-tab":
|
||||
return {
|
||||
title: "确定关闭标签?",
|
||||
description: `当前脚本有未保存修改,确定关闭 "${pending.name}" 吗?`,
|
||||
confirmLabel: "关闭",
|
||||
destructive: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function ScriptsPendingConfirmDialog({
|
||||
pending,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: ScriptsPendingConfirmDialogProps) {
|
||||
const copy = pending ? copyFor(pending) : null;
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={pending !== null}
|
||||
onOpenChange={onOpenChange}
|
||||
title={copy?.title ?? ""}
|
||||
description={copy?.description ?? ""}
|
||||
confirmLabel={copy?.confirmLabel}
|
||||
destructive={copy?.destructive}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -151,7 +151,10 @@ export function TreeContextMenu({
|
||||
className={dangerItemClass}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => onRemoveScript(contextMenu.script!)}
|
||||
onClick={() => {
|
||||
onRemoveScript(contextMenu.script!);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
删除文件
|
||||
@@ -204,7 +207,10 @@ export function TreeContextMenu({
|
||||
className={dangerItemClass}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => onRemoveDirectory(contextMenu.path)}
|
||||
onClick={() => {
|
||||
onRemoveDirectory(contextMenu.path);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
删除文件夹
|
||||
|
||||
@@ -164,7 +164,7 @@ export const createEditSessionSlice: StateCreator<
|
||||
const scriptId = active?.script_id;
|
||||
if (!active) {
|
||||
if (closeTabFlag && scriptId) {
|
||||
await get().closeTab(scriptId);
|
||||
await get().closeTab(scriptId, undefined, { discardDirty: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export const createEditSessionSlice: StateCreator<
|
||||
applyEditSessionState((p) => set(p), null, null);
|
||||
if (scriptId) sessionCache.delete(scriptId);
|
||||
if (closeTabFlag && scriptId) {
|
||||
await get().closeTab(scriptId);
|
||||
await get().closeTab(scriptId, undefined, { discardDirty: true });
|
||||
}
|
||||
if (showToast) {
|
||||
pushToast("success", `${active.script_name} 的编辑锁已释放`);
|
||||
|
||||
@@ -215,9 +215,6 @@ export const createMutationsSlice: StateCreator<
|
||||
deleteDataResource: async (resourceId) => {
|
||||
const api = requireApi();
|
||||
useUiStore.getState().closeContextMenu();
|
||||
if (!window.confirm(`确定删除数据资源吗?稳定版本会保留。`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.deleteResource(resourceId);
|
||||
set((state) => ({
|
||||
@@ -287,11 +284,6 @@ export const createMutationsSlice: StateCreator<
|
||||
deleteScript: async (script: ScriptItem) => {
|
||||
const api = requireApi();
|
||||
useUiStore.getState().closeContextMenu();
|
||||
if (
|
||||
!window.confirm(`确定删除文件"${script.script_name}"吗?稳定版本会保留。`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const editSession = getEditSession();
|
||||
if (editSession?.script_id === script.script_id) {
|
||||
await get().endEditing(false, false);
|
||||
@@ -323,11 +315,6 @@ export const createMutationsSlice: StateCreator<
|
||||
deleteDirectory: async (path) => {
|
||||
const api = requireApi();
|
||||
useUiStore.getState().closeContextMenu();
|
||||
if (
|
||||
!window.confirm(`确定递归删除文件夹"${path}"及其内容吗?稳定版本会保留。`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const activeScript = get().scripts.find(
|
||||
(item) => item.script_id === getEditSession()?.script_id,
|
||||
);
|
||||
|
||||
@@ -62,14 +62,11 @@ export const createSelectionSlice: StateCreator<
|
||||
}));
|
||||
},
|
||||
|
||||
closeTab: async (id, event) => {
|
||||
closeTab: async (id, event, options) => {
|
||||
event?.stopPropagation();
|
||||
const buffer = get().pythonEditorBuffers[id];
|
||||
if (buffer?.dirty && !buffer.saving) {
|
||||
const name =
|
||||
get().scripts.find((s) => s.script_id === id)?.script_name ?? "该脚本";
|
||||
const ok = window.confirm(`当前脚本有未保存修改,确定关闭 "${name}" 吗?`);
|
||||
if (!ok) return;
|
||||
if (buffer?.dirty && !buffer.saving && !options?.discardDirty) {
|
||||
return false;
|
||||
}
|
||||
if (buffer) {
|
||||
get().exitPythonEditor(id);
|
||||
@@ -91,6 +88,7 @@ export const createSelectionSlice: StateCreator<
|
||||
}
|
||||
return { openTabIds: newTabs, selectedId: nextSelected };
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
switchTab: (id) => {
|
||||
|
||||
@@ -92,7 +92,8 @@ export type SelectionSliceActions = {
|
||||
closeTab: (
|
||||
id: string,
|
||||
event?: { stopPropagation: () => void },
|
||||
) => Promise<void>;
|
||||
options?: { discardDirty?: boolean },
|
||||
) => Promise<boolean>;
|
||||
switchTab: (id: string) => void;
|
||||
openPublishDialog: (script: ScriptItem) => void;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import type { ScriptItem } from "~/services/api";
|
||||
import type { ScriptsPendingConfirm } from "./ScriptsPendingConfirm";
|
||||
|
||||
type DeleteActions = {
|
||||
deleteScript: (script: ScriptItem) => Promise<void>;
|
||||
deleteDataResource: (resourceId: string) => Promise<void>;
|
||||
deleteDirectory: (path: string) => Promise<void>;
|
||||
closeTab: (
|
||||
id: string,
|
||||
event?: { stopPropagation: () => void },
|
||||
options?: { discardDirty?: boolean },
|
||||
) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export function useScriptsPendingConfirm(
|
||||
scripts: ScriptItem[],
|
||||
actions: DeleteActions,
|
||||
) {
|
||||
const [pendingConfirm, setPendingConfirm] =
|
||||
useState<ScriptsPendingConfirm | null>(null);
|
||||
|
||||
const requestCloseTab = async (
|
||||
scriptId: string,
|
||||
event?: { stopPropagation: () => void },
|
||||
) => {
|
||||
const closed = await actions.closeTab(scriptId, event);
|
||||
if (closed) return;
|
||||
const name =
|
||||
scripts.find((s) => s.script_id === scriptId)?.script_name ?? "该脚本";
|
||||
setPendingConfirm({ kind: "close-tab", id: scriptId, name });
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!pendingConfirm) return;
|
||||
const target = pendingConfirm;
|
||||
setPendingConfirm(null);
|
||||
if (target.kind === "script") await actions.deleteScript(target.script);
|
||||
else if (target.kind === "resource") {
|
||||
await actions.deleteDataResource(target.id);
|
||||
} else if (target.kind === "directory") {
|
||||
await actions.deleteDirectory(target.path);
|
||||
} else {
|
||||
await actions.closeTab(target.id, undefined, { discardDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pendingConfirm,
|
||||
setPendingConfirm,
|
||||
requestCloseTab,
|
||||
handleConfirm,
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
type ScheduleNode,
|
||||
} from "~/services/api";
|
||||
|
||||
import { SchedulePendingConfirmDialog, type SchedulePendingConfirm } from "./SchedulePendingConfirm";
|
||||
import { ScheduleRenameDialog } from "./ScheduleRenameDialog";
|
||||
import { useApi, useAuth } from "~/context/AuthContext";
|
||||
import {
|
||||
formFieldClass,
|
||||
@@ -236,6 +238,8 @@ export default function SchedulePage({
|
||||
const setRuns = useSchedulesStore((s) => s.setRuns);
|
||||
const setRunsLoading = useSchedulesStore((s) => s.setRunsLoading);
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||
const [pendingConfirm, setPendingConfirm] =
|
||||
useState<SchedulePendingConfirm | null>(null);
|
||||
|
||||
const selectedNode = schedule?.nodes.find(
|
||||
(item) => item.node_id === selectedNodeId,
|
||||
@@ -366,6 +370,30 @@ export default function SchedulePage({
|
||||
[],
|
||||
);
|
||||
|
||||
const requestRemoveNode = async (node: ScheduleNode) => {
|
||||
const result = await useSchedulesStore.getState().removeNode(node);
|
||||
if (result === "needs_history_confirm") {
|
||||
setPendingConfirm({ kind: "node-history", node });
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!pendingConfirm) return;
|
||||
const target = pendingConfirm;
|
||||
setPendingConfirm(null);
|
||||
if (target.kind === "schedule") {
|
||||
await useSchedulesStore.getState().removeSchedule(target.schedule);
|
||||
} else if (target.kind === "artifact") {
|
||||
await useSchedulesStore.getState().removeArtifact(target.artifact);
|
||||
} else if (target.kind === "node") {
|
||||
await requestRemoveNode(target.node);
|
||||
} else {
|
||||
await useSchedulesStore
|
||||
.getState()
|
||||
.removeNode(target.node, { deleteExecutionHistory: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
|
||||
<header className="flex min-h-[56px] items-center justify-between rounded-[7px] border border-line bg-white px-[14px] shadow-md">
|
||||
@@ -416,7 +444,9 @@ export default function SchedulePage({
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!schedule || Boolean(busy)}
|
||||
onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)}
|
||||
onClick={() => {
|
||||
if (schedule) setPendingConfirm({ kind: "schedule", schedule });
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
@@ -662,7 +692,8 @@ export default function SchedulePage({
|
||||
busy={Boolean(busy)}
|
||||
onChange={setNodeForm}
|
||||
onSave={() => useSchedulesStore.getState().saveNode(selectedNode)}
|
||||
onDelete={() => useSchedulesStore.getState().removeNode(selectedNode)}
|
||||
onDelete={() =>
|
||||
setPendingConfirm({ kind: "node", node: selectedNode })}
|
||||
/>
|
||||
) : (
|
||||
<ScheduleInspector
|
||||
@@ -722,7 +753,8 @@ export default function SchedulePage({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)}
|
||||
onClick={() =>
|
||||
useSchedulesStore.getState().openRenameDialog(contextMenu.schedule)}
|
||||
>
|
||||
<Settings size={14} />
|
||||
改名
|
||||
@@ -732,21 +764,47 @@ export default function SchedulePage({
|
||||
type="button"
|
||||
variant="destructive"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)}
|
||||
onClick={() => {
|
||||
useSchedulesStore.getState().setContextMenu(null);
|
||||
setPendingConfirm({
|
||||
kind: "schedule",
|
||||
schedule: contextMenu.schedule,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
删除调度方案
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{contextMenu.kind === "artifact" && null}
|
||||
{contextMenu.kind === "artifact" && (
|
||||
<Button
|
||||
className="w-full justify-start gap-[9px] rounded-[5px] p-[9px_10px] text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
useSchedulesStore.getState().setContextMenu(null);
|
||||
setPendingConfirm({
|
||||
kind: "artifact",
|
||||
artifact: contextMenu.artifact,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
移出调度列表
|
||||
</Button>
|
||||
)}
|
||||
{contextMenu.kind === "node" && (
|
||||
<Button
|
||||
className="w-full justify-start gap-[9px] rounded-[5px] p-[9px_10px] text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
role="menuitem"
|
||||
onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)}
|
||||
onClick={() => {
|
||||
useSchedulesStore.getState().setContextMenu(null);
|
||||
setPendingConfirm({ kind: "node", node: contextMenu.node });
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
删除画布节点
|
||||
@@ -816,6 +874,16 @@ export default function SchedulePage({
|
||||
</form>
|
||||
</AppFormDialog>
|
||||
|
||||
<ScheduleRenameDialog />
|
||||
|
||||
<SchedulePendingConfirmDialog
|
||||
pending={pendingConfirm}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPendingConfirm(null);
|
||||
}}
|
||||
onConfirm={() => void handleConfirm()}
|
||||
/>
|
||||
|
||||
{busy && (
|
||||
<div
|
||||
className="absolute bottom-[22px] right-[22px] z-[20] flex min-h-[38px] items-center gap-2 rounded-[7px] border border-[#c9dcee] bg-white px-[13px] text-[11px] text-[#4e657d] shadow-lg"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||
import type {
|
||||
Schedule,
|
||||
ScheduleArtifact,
|
||||
ScheduleNode,
|
||||
} from "~/services/api";
|
||||
|
||||
export type SchedulePendingConfirm =
|
||||
| { kind: "schedule"; schedule: Schedule }
|
||||
| { kind: "artifact"; artifact: ScheduleArtifact }
|
||||
| { kind: "node"; node: ScheduleNode }
|
||||
| { kind: "node-history"; node: ScheduleNode };
|
||||
|
||||
type SchedulePendingConfirmDialogProps = {
|
||||
pending: SchedulePendingConfirm | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function copyFor(pending: SchedulePendingConfirm) {
|
||||
switch (pending.kind) {
|
||||
case "schedule":
|
||||
return {
|
||||
title: "确定删除调度?",
|
||||
description: `确定删除调度"${pending.schedule.schedule_name}"吗?`,
|
||||
confirmLabel: "删除",
|
||||
};
|
||||
case "artifact":
|
||||
return {
|
||||
title: "确定移出调度列表?",
|
||||
description:
|
||||
`确定将"${pending.artifact.script_name} ${pending.artifact.version_label}"移出调度列表吗?稳定版本本身和历史运行记录不会被删除。`,
|
||||
confirmLabel: "移出",
|
||||
};
|
||||
case "node":
|
||||
return {
|
||||
title: "确定删除节点?",
|
||||
description: `确定删除节点"${pending.node.node_name}"吗?`,
|
||||
confirmLabel: "删除",
|
||||
};
|
||||
case "node-history":
|
||||
return {
|
||||
title: "一并删除运行日志?",
|
||||
description: "该节点有运行日志,是否一并删除?",
|
||||
confirmLabel: "删除",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function SchedulePendingConfirmDialog({
|
||||
pending,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: SchedulePendingConfirmDialogProps) {
|
||||
const copy = pending ? copyFor(pending) : null;
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={pending !== null}
|
||||
onOpenChange={onOpenChange}
|
||||
title={copy?.title ?? ""}
|
||||
description={copy?.description ?? ""}
|
||||
confirmLabel={copy?.confirmLabel}
|
||||
destructive
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { type FormEvent } from "react";
|
||||
import { Check, Loader2Icon } from "lucide-react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
AppFormDialog,
|
||||
dialogPrimaryButtonClass,
|
||||
dialogSecondaryButtonClass,
|
||||
} from "~/components/common/AppFormDialog";
|
||||
import {
|
||||
formFieldClass,
|
||||
formInputClass,
|
||||
modalFormClass,
|
||||
} from "~/features/platform/modalUi";
|
||||
|
||||
import { useSchedulesStore } from "./state/useSchedulesStore";
|
||||
|
||||
export function ScheduleRenameDialog() {
|
||||
const renameTarget = useSchedulesStore((s) => s.renameTarget);
|
||||
const renameName = useSchedulesStore((s) => s.renameName);
|
||||
const busy = useSchedulesStore((s) => s.busy);
|
||||
const setRenameName = useSchedulesStore((s) => s.setRenameName);
|
||||
const closeRenameDialog = useSchedulesStore((s) => s.closeRenameDialog);
|
||||
|
||||
const renaming = busy === "rename-schedule";
|
||||
const trimmed = renameName.trim();
|
||||
const unchanged = Boolean(
|
||||
renameTarget && trimmed === renameTarget.schedule_name,
|
||||
);
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!renameTarget || !trimmed || unchanged) return;
|
||||
void useSchedulesStore.getState().renameSchedule(renameTarget, trimmed);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppFormDialog
|
||||
open={renameTarget !== null}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) closeRenameDialog();
|
||||
}}
|
||||
eyebrow="SCHEDULE"
|
||||
title="改名调度方案"
|
||||
titleId="rename-schedule-title"
|
||||
>
|
||||
<form className={modalFormClass} onSubmit={handleSubmit}>
|
||||
<label className={formFieldClass}>
|
||||
<span>调度方案名称</span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
placeholder="请输入新的调度方案名称"
|
||||
value={renameName}
|
||||
onChange={(event) => setRenameName(event.target.value)}
|
||||
onFocus={(event) => event.currentTarget.select()}
|
||||
/>
|
||||
</label>
|
||||
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-line bg-white -mx-[22px] px-[22px] py-3.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={renaming}
|
||||
onClick={closeRenameDialog}
|
||||
className={dialogSecondaryButtonClass}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
size="sm"
|
||||
disabled={renaming || !trimmed || unchanged}
|
||||
className={dialogPrimaryButtonClass}
|
||||
>
|
||||
{renaming
|
||||
? <Loader2Icon className="size-4 animate-spin" />
|
||||
: <Check size={15} />}
|
||||
{renaming ? "正在保存…" : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppFormDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
// Dialog slice: create-schedule dialog state + cron preview.
|
||||
// Pure setters + small mutations that read/write only dialog-owned fields.
|
||||
|
||||
import type { CronPreview } from "../../../services/api";
|
||||
import type { CronPreview, Schedule } from "../../../services/api";
|
||||
import type { StateCreator } from "zustand";
|
||||
|
||||
import { handleError, notify, requireApi } from "./helpers";
|
||||
@@ -12,6 +9,8 @@ import type { SchedulesStore } from "./useSchedulesStore";
|
||||
export type DialogSliceState = {
|
||||
createDialogOpen: boolean;
|
||||
newScheduleName: string;
|
||||
renameTarget: Schedule | null;
|
||||
renameName: string;
|
||||
cronResult: CronPreview | null;
|
||||
};
|
||||
|
||||
@@ -21,10 +20,13 @@ export type DialogSliceActions = {
|
||||
// pure setters
|
||||
setCreateDialogOpen: (open: boolean) => void;
|
||||
setNewScheduleName: (name: string) => void;
|
||||
setRenameName: (name: string) => void;
|
||||
setCronResult: (result: CronPreview | null) => void;
|
||||
|
||||
// mutations
|
||||
openCreateDialog: () => void;
|
||||
openRenameDialog: (target: Schedule) => void;
|
||||
closeRenameDialog: () => void;
|
||||
runCronPreview: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -35,12 +37,15 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
||||
// ---- initial state ----
|
||||
createDialogOpen: false,
|
||||
newScheduleName: "",
|
||||
renameTarget: null,
|
||||
renameName: "",
|
||||
cronResult: null,
|
||||
|
||||
// ---- pure setters ----
|
||||
|
||||
setCreateDialogOpen: (open) => set({ createDialogOpen: open }),
|
||||
setNewScheduleName: (name) => set({ newScheduleName: name }),
|
||||
setRenameName: (name) => set({ renameName: name }),
|
||||
setCronResult: (result) => set({ cronResult: result }),
|
||||
|
||||
// ---- mutations ----
|
||||
@@ -54,6 +59,20 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
||||
}));
|
||||
},
|
||||
|
||||
openRenameDialog: (target) => {
|
||||
if (get().busy) return;
|
||||
set({
|
||||
contextMenu: null,
|
||||
renameTarget: target,
|
||||
renameName: target.schedule_name,
|
||||
});
|
||||
},
|
||||
|
||||
closeRenameDialog: () => {
|
||||
if (get().busy === "rename-schedule") return;
|
||||
set({ renameTarget: null, renameName: "" });
|
||||
},
|
||||
|
||||
runCronPreview: async () => {
|
||||
const api = requireApi();
|
||||
const { scheduleForm } = get();
|
||||
@@ -75,4 +94,4 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
||||
set({ busy: null });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ export type ListSliceActions = {
|
||||
// mutations
|
||||
addSchedule: (name: string) => Promise<void>;
|
||||
removeSchedule: (target?: Schedule) => Promise<void>;
|
||||
renameSchedule: (target: Schedule) => Promise<void>;
|
||||
renameSchedule: (target: Schedule, scheduleName: string) => Promise<void>;
|
||||
removeArtifact: (artifact: ScheduleArtifact) => Promise<void>;
|
||||
saveSchedule: () => Promise<void>;
|
||||
runNow: () => Promise<void>;
|
||||
@@ -70,7 +70,10 @@ export type ListSliceActions = {
|
||||
positionY: number,
|
||||
) => Promise<void>;
|
||||
saveNode: (selectedNode: ScheduleNode | null) => Promise<void>;
|
||||
removeNode: (target?: ScheduleNode) => Promise<void>;
|
||||
removeNode: (
|
||||
target?: ScheduleNode,
|
||||
options?: { deleteExecutionHistory?: boolean },
|
||||
) => Promise<"ok" | "needs_history_confirm" | "noop">;
|
||||
removeEdge: (target?: ScheduleEdge) => Promise<void>;
|
||||
checkDag: () => Promise<void>;
|
||||
connectTo: (targetNodeId: string) => Promise<void>;
|
||||
@@ -234,7 +237,6 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
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);
|
||||
@@ -263,19 +265,15 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
}
|
||||
},
|
||||
|
||||
renameSchedule: async (target) => {
|
||||
renameSchedule: async (target, scheduleName) => {
|
||||
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;
|
||||
const trimmed = scheduleName.trim();
|
||||
if (get().busy || !trimmed || trimmed === 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,
|
||||
schedule_name: trimmed,
|
||||
});
|
||||
set((s: any) => ({
|
||||
schedules: s.schedules.map((item: Schedule) =>
|
||||
@@ -287,6 +285,8 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
s.schedule?.schedule_id === updated.schedule_id
|
||||
? updated
|
||||
: s.schedule,
|
||||
renameTarget: null,
|
||||
renameName: "",
|
||||
}));
|
||||
notify({ tone: "success", message: "调度方案已改名" });
|
||||
} catch (error) {
|
||||
@@ -301,12 +301,6 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
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);
|
||||
@@ -531,16 +525,15 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
}
|
||||
},
|
||||
|
||||
removeNode: async (target) => {
|
||||
removeNode: async (target, options) => {
|
||||
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;
|
||||
if (!schedule || !node || state_.busy) return "noop";
|
||||
set({ contextMenu: null });
|
||||
if (!window.confirm(`确定删除节点"${node.node_name}"吗?`)) return;
|
||||
set({ busy: "delete-node" });
|
||||
try {
|
||||
let updated: Schedule;
|
||||
@@ -550,6 +543,9 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
schedule.schedule_id,
|
||||
node.node_id,
|
||||
schedule.workflow_version,
|
||||
options?.deleteExecutionHistory
|
||||
? { delete_execution_history: true }
|
||||
: undefined,
|
||||
);
|
||||
} catch (error) {
|
||||
const requiresHistoryConfirmation =
|
||||
@@ -557,7 +553,10 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
&& error.status === 409
|
||||
&& error.code === "node_execution_history_exists";
|
||||
if (!requiresHistoryConfirmation) throw error;
|
||||
if (!window.confirm("该节点有运行日志,是否一并删除?")) return;
|
||||
// 交给 UI 弹二次确认;已确认则带 delete_execution_history 重试。
|
||||
if (!options?.deleteExecutionHistory) {
|
||||
return "needs_history_confirm";
|
||||
}
|
||||
updated = await api.deleteScheduleNode(
|
||||
schedule.schedule_id,
|
||||
node.node_id,
|
||||
@@ -568,9 +567,11 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
||||
applyServerUpdatedSchedule(set, updated);
|
||||
if (selectedNodeId === node.node_id) set({ selectedNodeId: null });
|
||||
notify({ tone: "success", message: "节点及其运行日志已删除" });
|
||||
return "ok";
|
||||
} catch (error) {
|
||||
const { handleError } = await import("./helpers");
|
||||
await handleError(get, error, "删除节点失败");
|
||||
return "noop";
|
||||
} finally {
|
||||
set({ busy: null });
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ const INITIAL: Omit<SchedulesState, "schedule" | "busy"> = {
|
||||
// Dialog
|
||||
createDialogOpen: false,
|
||||
newScheduleName: "",
|
||||
renameTarget: null,
|
||||
renameName: "",
|
||||
cronResult: null,
|
||||
// Runs
|
||||
runs: [],
|
||||
|
||||
Reference in New Issue
Block a user