fix: prevent async confirmation races in API key deletion

This commit is contained in:
Supra4E8C
2026-01-17 15:31:35 +08:00
parent b8d7b8997c
commit 7b0d6dc7e9
4 changed files with 53 additions and 5 deletions

View File

@@ -32,6 +32,9 @@ export function ConfirmationModal() {
};
const handleCancel = () => {
if (isLoading) {
return;
}
if (onCancel) {
onCancel();
}
@@ -39,7 +42,7 @@ export function ConfirmationModal() {
};
return (
<Modal open={isOpen} onClose={handleCancel} title={title}>
<Modal open={isOpen} onClose={handleCancel} title={title} closeDisabled={isLoading}>
<p style={{ margin: '1rem 0' }}>{message}</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
<Button variant="ghost" onClick={handleCancel} disabled={isLoading}>

View File

@@ -8,6 +8,7 @@ interface ModalProps {
onClose: () => void;
footer?: ReactNode;
width?: number | string;
closeDisabled?: boolean;
}
const CLOSE_ANIMATION_DURATION = 350;
@@ -32,7 +33,15 @@ const unlockScroll = () => {
}
};
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
export function Modal({
open,
title,
onClose,
footer,
width = 520,
closeDisabled = false,
children
}: PropsWithChildren<ModalProps>) {
const [isVisible, setIsVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -106,7 +115,13 @@ export function Modal({ open, title, onClose, footer, width = 520, children }: P
const modalContent = (
<div className={overlayClass}>
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
<button className="modal-close-floating" onClick={handleClose} aria-label="Close">
<button
type="button"
className="modal-close-floating"
onClick={closeDisabled ? undefined : handleClose}
aria-label="Close"
disabled={closeDisabled}
>
<IconX size={20} />
</button>
<div className="modal-header">

View File

@@ -115,14 +115,32 @@ export function ApiKeysPage() {
};
const handleDelete = (index: number) => {
const apiKeyToDelete = apiKeys[index];
if (!apiKeyToDelete) {
showNotification(t('notification.delete_failed'), 'error');
return;
}
showConfirmation({
title: t('common.delete'),
message: t('api_keys.delete_confirm'),
variant: 'danger',
onConfirm: async () => {
const latestKeys = useConfigStore.getState().config?.apiKeys;
const currentKeys = Array.isArray(latestKeys) ? latestKeys : [];
const deleteIndex =
currentKeys[index] === apiKeyToDelete
? index
: currentKeys.findIndex((key) => key === apiKeyToDelete);
if (deleteIndex < 0) {
showNotification(t('notification.delete_failed'), 'error');
return;
}
try {
await apiKeysApi.delete(index);
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
await apiKeysApi.delete(deleteIndex);
const nextKeys = currentKeys.filter((_, idx) => idx !== deleteIndex);
setApiKeys(nextKeys);
updateConfigValue('api-keys', nextKeys);
clearCache('api-keys');

View File

@@ -453,6 +453,18 @@ textarea {
&:active {
transform: scale(0.95);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
&:disabled:hover {
color: var(--text-secondary);
background: var(--bg-secondary);
transform: none;
}
}
.modal-header {