From b8d7b8997c0a3ef839a5aae352560cdc76ff7016 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 17 Jan 2026 14:59:46 +0800 Subject: [PATCH] feat(ui): implement global ConfirmationModal to replace native window.confirm --- src/App.tsx | 2 + src/components/common/ConfirmationModal.tsx | 58 +++++++++++++++++++++ src/pages/ApiKeysPage.tsx | 39 +++++++------- src/stores/useNotificationStore.ts | 52 ++++++++++++++++++ 4 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 src/components/common/ConfirmationModal.tsx diff --git a/src/App.tsx b/src/App.tsx index 14afc87..54f3770 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { HashRouter, Route, Routes } from 'react-router-dom'; import { LoginPage } from '@/pages/LoginPage'; import { NotificationContainer } from '@/components/common/NotificationContainer'; +import { ConfirmationModal } from '@/components/common/ConfirmationModal'; import { SplashScreen } from '@/components/common/SplashScreen'; import { MainLayout } from '@/components/layout/MainLayout'; import { ProtectedRoute } from '@/router/ProtectedRoute'; @@ -61,6 +62,7 @@ function App() { return ( + } /> state.confirmation); + const hideConfirmation = useNotificationStore((state) => state.hideConfirmation); + const setConfirmationLoading = useNotificationStore((state) => state.setConfirmationLoading); + + const { isOpen, isLoading, options } = confirmation; + + if (!isOpen || !options) { + return null; + } + + const { title, message, onConfirm, onCancel, confirmText, cancelText, variant = 'primary' } = options; + + const handleConfirm = async () => { + try { + setConfirmationLoading(true); + await onConfirm(); + hideConfirmation(); + } catch (error) { + console.error('Confirmation action failed:', error); + // Optional: show error notification here if needed, + // but usually the calling component handles specific errors. + } finally { + setConfirmationLoading(false); + } + }; + + const handleCancel = () => { + if (onCancel) { + onCancel(); + } + hideConfirmation(); + }; + + return ( + +

{message}

+
+ + +
+
+ ); +} diff --git a/src/pages/ApiKeysPage.tsx b/src/pages/ApiKeysPage.tsx index 3461151..8fe8d28 100644 --- a/src/pages/ApiKeysPage.tsx +++ b/src/pages/ApiKeysPage.tsx @@ -14,7 +14,7 @@ import styles from './ApiKeysPage.module.scss'; export function ApiKeysPage() { const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const config = useConfigStore((state) => state.config); @@ -29,7 +29,6 @@ export function ApiKeysPage() { const [editingIndex, setEditingIndex] = useState(null); const [inputValue, setInputValue] = useState(''); const [saving, setSaving] = useState(false); - const [deletingIndex, setDeletingIndex] = useState(null); const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]); @@ -115,21 +114,24 @@ export function ApiKeysPage() { } }; - const handleDelete = async (index: number) => { - if (!window.confirm(t('api_keys.delete_confirm'))) return; - setDeletingIndex(index); - try { - await apiKeysApi.delete(index); - const nextKeys = apiKeys.filter((_, idx) => idx !== index); - setApiKeys(nextKeys); - updateConfigValue('api-keys', nextKeys); - clearCache('api-keys'); - showNotification(t('notification.api_key_deleted'), 'success'); - } catch (err: any) { - showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); - } finally { - setDeletingIndex(null); - } + const handleDelete = (index: number) => { + showConfirmation({ + title: t('common.delete'), + message: t('api_keys.delete_confirm'), + variant: 'danger', + onConfirm: async () => { + try { + await apiKeysApi.delete(index); + const nextKeys = apiKeys.filter((_, idx) => idx !== index); + setApiKeys(nextKeys); + updateConfigValue('api-keys', nextKeys); + clearCache('api-keys'); + showNotification(t('notification.api_key_deleted'), 'success'); + } catch (err: any) { + showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); + } + } + }); }; const actionButtons = ( @@ -181,8 +183,7 @@ export function ApiKeysPage() { variant="danger" size="sm" onClick={() => handleDelete(index)} - disabled={disableControls || deletingIndex === index} - loading={deletingIndex === index} + disabled={disableControls} > {t('common.delete')} diff --git a/src/stores/useNotificationStore.ts b/src/stores/useNotificationStore.ts index d0ab70a..17971c5 100644 --- a/src/stores/useNotificationStore.ts +++ b/src/stores/useNotificationStore.ts @@ -8,15 +8,38 @@ import type { Notification, NotificationType } from '@/types'; import { generateId } from '@/utils/helpers'; import { NOTIFICATION_DURATION_MS } from '@/utils/constants'; +interface ConfirmationOptions { + title?: string; + message: string; + confirmText?: string; + cancelText?: string; + variant?: 'danger' | 'primary' | 'secondary'; + onConfirm: () => void | Promise; + onCancel?: () => void; +} + interface NotificationState { notifications: Notification[]; + confirmation: { + isOpen: boolean; + isLoading: boolean; + options: ConfirmationOptions | null; + }; showNotification: (message: string, type?: NotificationType, duration?: number) => void; removeNotification: (id: string) => void; clearAll: () => void; + showConfirmation: (options: ConfirmationOptions) => void; + hideConfirmation: () => void; + setConfirmationLoading: (loading: boolean) => void; } export const useNotificationStore = create((set) => ({ notifications: [], + confirmation: { + isOpen: false, + isLoading: false, + options: null + }, showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => { const id = generateId(); @@ -49,5 +72,34 @@ export const useNotificationStore = create((set) => ({ clearAll: () => { set({ notifications: [] }); + }, + + showConfirmation: (options) => { + set({ + confirmation: { + isOpen: true, + isLoading: false, + options + } + }); + }, + + hideConfirmation: () => { + set((state) => ({ + confirmation: { + ...state.confirmation, + isOpen: false, + options: null // Cleanup + } + })); + }, + + setConfirmationLoading: (loading) => { + set((state) => ({ + confirmation: { + ...state.confirmation, + isLoading: loading + } + })); } }));