fix: prevent page crash when deleting an agent (#514)

This commit is contained in:
paisley
2026-03-16 10:47:30 +08:00
committed by GitHub
Unverified
parent 9e10c12f67
commit 158e84ce8f
5 changed files with 46 additions and 9 deletions

View File

@@ -2,7 +2,7 @@
* ConfirmDialog - In-DOM confirmation dialog (replaces window.confirm)
* Keeps focus within the renderer to avoid Windows focus loss after native dialogs.
*/
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
@@ -13,8 +13,9 @@ interface ConfirmDialogProps {
confirmLabel?: string;
cancelLabel?: string;
variant?: 'default' | 'destructive';
onConfirm: () => void;
onConfirm: () => void | Promise<void>;
onCancel: () => void;
onError?: (error: unknown) => void;
}
export function ConfirmDialog({
@@ -26,8 +27,19 @@ export function ConfirmDialog({
variant = 'default',
onConfirm,
onCancel,
onError,
}: ConfirmDialogProps) {
const cancelRef = useRef<HTMLButtonElement>(null);
const [confirming, setConfirming] = useState(false);
const [prevOpen, setPrevOpen] = useState(open);
// Reset confirming when dialog closes (during render to avoid setState-in-effect)
if (prevOpen !== open) {
setPrevOpen(open);
if (!open) {
setConfirming(false);
}
}
useEffect(() => {
if (open && cancelRef.current) {
@@ -38,12 +50,27 @@ export function ConfirmDialog({
if (!open) return null;
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
if (e.key === 'Escape' && !confirming) {
e.preventDefault();
onCancel();
}
};
const handleConfirm = () => {
if (confirming) return;
const result = onConfirm();
if (result instanceof Promise) {
setConfirming(true);
result.catch((error) => {
if (onError) {
onError(error);
}
}).finally(() => {
setConfirming(false);
});
}
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@@ -68,12 +95,14 @@ export function ConfirmDialog({
ref={cancelRef}
variant="outline"
onClick={onCancel}
disabled={confirming}
>
{cancelLabel}
</Button>
<Button
variant={variant === 'destructive' ? 'destructive' : 'default'}
onClick={onConfirm}
onClick={handleConfirm}
disabled={confirming}
>
{confirmLabel}
</Button>

View File

@@ -41,6 +41,7 @@
"agentCreated": "Agent created",
"agentCreateFailed": "Failed to create agent: {{error}}",
"agentDeleted": "Agent deleted",
"agentDeleteFailed": "Failed to delete agent: {{error}}",
"agentUpdated": "Agent updated",
"agentUpdateFailed": "Failed to update agent: {{error}}",
"channelAssigned": "{{channel}} assigned to agent",

View File

@@ -41,6 +41,7 @@
"agentCreated": "Agent を作成しました",
"agentCreateFailed": "Agent の作成に失敗しました: {{error}}",
"agentDeleted": "Agent を削除しました",
"agentDeleteFailed": "Agent の削除に失敗しました: {{error}}",
"agentUpdated": "Agent を更新しました",
"agentUpdateFailed": "Agent の更新に失敗しました: {{error}}",
"channelAssigned": "{{channel}} を Agent に割り当てました",

View File

@@ -41,6 +41,7 @@
"agentCreated": "Agent 已创建",
"agentCreateFailed": "创建 Agent 失败:{{error}}",
"agentDeleted": "Agent 已删除",
"agentDeleteFailed": "删除 Agent 失败:{{error}}",
"agentUpdated": "Agent 已更新",
"agentUpdateFailed": "更新 Agent 失败:{{error}}",
"channelAssigned": "{{channel}} 已分配给 Agent",

View File

@@ -153,12 +153,17 @@ export function Agents() {
variant="destructive"
onConfirm={async () => {
if (!agentToDelete) return;
await deleteAgent(agentToDelete.id);
setAgentToDelete(null);
if (activeAgentId === agentToDelete.id) {
setActiveAgentId(null);
try {
await deleteAgent(agentToDelete.id);
const deletedId = agentToDelete.id;
setAgentToDelete(null);
if (activeAgentId === deletedId) {
setActiveAgentId(null);
}
toast.success(t('toast.agentDeleted'));
} catch (error) {
toast.error(t('toast.agentDeleteFailed', { error: String(error) }));
}
toast.success(t('toast.agentDeleted'));
}}
onCancel={() => setAgentToDelete(null)}
/>