feat: add CronDeleteDialog component and integrate it into CronPage for job deletion
- Implemented a new CronDeleteDialog component for confirming job deletions. - Integrated the CronDeleteDialog into the CronPage, allowing users to delete cron jobs with confirmation. - Refactored job deletion logic to handle state updates and loading indicators during deletion. - Removed unused delivery channel related code from CronPage and CronTaskDialog. - Cleaned up chat store session deletion logic to improve state management and ensure proper session handling.
This commit is contained in:
@@ -21,7 +21,7 @@ function ChatMessageList({ messages, loading, showWelcomeState }: ChatMessageLis
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-6 py-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden p-6">
|
||||
<div ref={containerRef} className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pr-1">
|
||||
{loading ? (
|
||||
<div className="rounded-[18px] border border-dashed border-[#BEDBFF] bg-[#EFF6FF] px-4 py-3 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
"deleted": "Task deleted."
|
||||
},
|
||||
"confirmDelete": "Delete “{name}”? This cannot be undone.",
|
||||
"deleteDialog": {
|
||||
"title": "Delete task",
|
||||
"label": "Task name",
|
||||
"confirm": "Confirm delete",
|
||||
"deleting": "Deleting..."
|
||||
},
|
||||
"common": {
|
||||
"unnamedJob": "Untitled task"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
"deleted": "ลบงานแล้ว"
|
||||
},
|
||||
"confirmDelete": "ต้องการลบ \"{name}\" หรือไม่? การดำเนินการนี้ไม่สามารถย้อนกลับได้",
|
||||
"deleteDialog": {
|
||||
"title": "ลบงาน",
|
||||
"label": "ชื่องาน",
|
||||
"confirm": "ยืนยันการลบ",
|
||||
"deleting": "กำลังลบ..."
|
||||
},
|
||||
"common": {
|
||||
"unnamedJob": "งานไม่มีชื่อ"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
"deleted": "任务已删除。"
|
||||
},
|
||||
"confirmDelete": "确认删除“{name}”吗?删除后将无法恢复。",
|
||||
"deleteDialog": {
|
||||
"title": "删除任务",
|
||||
"label": "任务名称",
|
||||
"confirm": "确认删除",
|
||||
"deleting": "删除中..."
|
||||
},
|
||||
"common": {
|
||||
"unnamedJob": "未命名任务"
|
||||
},
|
||||
|
||||
117
src/pages/Cron/components/CronDeleteDialog.tsx
Normal file
117
src/pages/Cron/components/CronDeleteDialog.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Loader2, Trash2, X } from 'lucide-react';
|
||||
import type { CronJob } from '../../../lib/cron-types';
|
||||
import { useI18n } from '../../../i18n';
|
||||
|
||||
type CronDeleteDialogProps = {
|
||||
open: boolean;
|
||||
job: CronJob | null;
|
||||
busy: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export default function CronDeleteDialog({
|
||||
open,
|
||||
job,
|
||||
busy,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: CronDeleteDialogProps) {
|
||||
const { t } = useI18n();
|
||||
const jobName = job?.name.trim() ? job.name : t('cron.common.unnamedJob');
|
||||
|
||||
function handleOpenChange(nextOpen: boolean): void {
|
||||
if (!nextOpen && !busy) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/45 backdrop-blur-[2px]" />
|
||||
<Dialog.Content
|
||||
className="fixed left-1/2 top-1/2 z-60 w-[calc(100vw-32px)] max-w-[560px] -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white p-0 shadow-[0_30px_80px_rgba(15,23,42,0.18)] outline-none dark:bg-[#1f1f22]"
|
||||
onEscapeKeyDown={(event) => {
|
||||
if (busy) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
if (busy) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 border-b border-black/6 px-6 py-5 dark:border-white/6">
|
||||
<div className="flex min-w-0 items-start gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-red-500/15 bg-red-500/10 text-red-600 dark:border-red-400/20 dark:bg-red-500/10 dark:text-red-300">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<Dialog.Title
|
||||
className="text-[26px] font-normal leading-none tracking-tight text-[#171717] dark:text-[#f3f4f6]"
|
||||
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
|
||||
>
|
||||
{t('cron.deleteDialog.title')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mt-3 text-[14px] leading-6 text-[#525866] dark:text-gray-400">
|
||||
{t('cron.confirmDelete', { name: jobName })}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-1.5 text-[#99A0AE] transition-colors hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-50 dark:hover:text-[#f3f4f6]"
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
aria-label={t('dialog.close')}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-5">
|
||||
<div className="rounded-[18px] border border-black/6 bg-white/75 px-4 py-3 shadow-[0_10px_30px_rgba(15,23,42,0.04)] dark:border-white/8 dark:bg-[#202024]">
|
||||
<div className="text-[12px] uppercase tracking-[0.14em] text-[#99A0AE] dark:text-gray-500">
|
||||
{t('cron.deleteDialog.label')}
|
||||
</div>
|
||||
<div className="mt-2 truncate text-[15px] font-medium text-[#171717] dark:text-[#f3f4f6]">
|
||||
{jobName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 items-center rounded-full border border-black/10 px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
>
|
||||
{t('dialog.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 items-center rounded-full bg-red-600 px-4 text-[13px] font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-red-500 dark:hover:bg-red-400"
|
||||
onClick={onConfirm}
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('cron.deleteDialog.deleting')}
|
||||
</>
|
||||
) : (
|
||||
t('cron.deleteDialog.confirm')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -656,31 +656,32 @@ function selectAgent(agentId: string): void {
|
||||
}
|
||||
|
||||
async function deleteSession(sessionKey: string): Promise<void> {
|
||||
if (sessionKey === state.currentSessionKey) {
|
||||
const currentState = state;
|
||||
const isDeletingCurrentSession = sessionKey === currentState.currentSessionKey;
|
||||
|
||||
if (isDeletingCurrentSession) {
|
||||
resetPendingStreamingDelta();
|
||||
}
|
||||
|
||||
try {
|
||||
await gatewayRpc('session.delete', { sessionKey });
|
||||
} catch {
|
||||
// keep local cleanup even if gateway delete fails
|
||||
}
|
||||
historyLoadInFlight.delete(sessionKey);
|
||||
lastHistoryLoadAtBySession.delete(sessionKey);
|
||||
|
||||
const remaining = state.sessions.filter((session) => session.key !== sessionKey);
|
||||
const remaining = currentState.sessions.filter((session) => session.key !== sessionKey);
|
||||
const basePatch: Partial<ChatStoreState> = {
|
||||
sessions: remaining,
|
||||
sessionLabels: clearSessionEntryFromMap(state.sessionLabels, sessionKey),
|
||||
sessionLastActivity: clearSessionEntryFromMap(state.sessionLastActivity, sessionKey),
|
||||
sessionLabels: clearSessionEntryFromMap(currentState.sessionLabels, sessionKey),
|
||||
sessionLastActivity: clearSessionEntryFromMap(currentState.sessionLastActivity, sessionKey),
|
||||
};
|
||||
|
||||
if (state.currentSessionKey === sessionKey) {
|
||||
const nextSession = remaining[0]?.key ?? getDefaultMainSessionKey();
|
||||
const hasRemainingSessions = remaining.length > 0;
|
||||
if (isDeletingCurrentSession) {
|
||||
const nextAgentId = getAgentIdFromSessionKey(sessionKey);
|
||||
patchState({
|
||||
...basePatch,
|
||||
currentSessionKey: nextSession,
|
||||
currentAgentId: getAgentIdFromSessionKey(nextSession),
|
||||
currentSessionKey: buildNewSessionKey(nextAgentId),
|
||||
currentAgentId: nextAgentId,
|
||||
messages: [],
|
||||
loading: false,
|
||||
sending: false,
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
activeRunId: null,
|
||||
@@ -688,14 +689,15 @@ async function deleteSession(sessionKey: string): Promise<void> {
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
});
|
||||
|
||||
if (hasRemainingSessions && nextSession) {
|
||||
await loadHistory(nextSession);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
patchState(basePatch);
|
||||
}
|
||||
|
||||
patchState(basePatch);
|
||||
try {
|
||||
await gatewayRpc('session.delete', { sessionKey });
|
||||
} catch {
|
||||
// keep local cleanup even if gateway delete fails
|
||||
}
|
||||
}
|
||||
|
||||
function renameSession(sessionKey: string, nextLabel: string): void {
|
||||
|
||||
Reference in New Issue
Block a user