From 2230f75d8a389b719933915059b8ff9ba023de90 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 9 Apr 2026 23:27:40 +0800 Subject: [PATCH] feat: add loading state management for TOTP and import/export operations --- webapp/src/App.tsx | 27 ++++++- webapp/src/components/AppGlobalOverlays.tsx | 8 +- webapp/src/components/BackupCenterPage.tsx | 12 +++ webapp/src/components/ImportPage.tsx | 9 +++ webapp/src/components/VaultPage.tsx | 1 + webapp/src/components/vault/VaultDialogs.tsx | 82 ++++++++++++++++++-- 6 files changed, 131 insertions(+), 8 deletions(-) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 5e89afe..0e83f62 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -172,9 +172,11 @@ export default function App() { const [pendingTotp, setPendingTotp] = useState(null); const [totpCode, setTotpCode] = useState(''); const [rememberDevice, setRememberDevice] = useState(true); + const [totpSubmitting, setTotpSubmitting] = useState(false); const [disableTotpOpen, setDisableTotpOpen] = useState(false); const [disableTotpPassword, setDisableTotpPassword] = useState(''); + const [disableTotpSubmitting, setDisableTotpSubmitting] = useState(false); const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' }); const [themePreference, setThemePreference] = useState(() => readThemePreference()); const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => resolveSystemTheme()); @@ -433,16 +435,20 @@ export default function App() { } async function handleTotpVerify() { + if (totpSubmitting) return; if (!pendingTotp) return; if (!totpCode.trim()) { pushToast('error', t('txt_please_input_totp_code')); return; } + setTotpSubmitting(true); try { const login = await performTotpLogin(pendingTotp, totpCode, rememberDevice); await finalizeLogin(login); } catch (error) { pushToast('error', error instanceof Error ? error.message : t('txt_totp_verify_failed')); + } finally { + setTotpSubmitting(false); } } @@ -631,11 +637,13 @@ export default function App() { onConfirmTotp={() => {}} onCancelTotp={() => {}} onUseRecoveryCode={() => {}} + totpSubmitting={false} disableTotpOpen={false} disableTotpPassword="" onDisableTotpPasswordChange={() => {}} onConfirmDisableTotp={() => {}} onCancelDisableTotp={() => {}} + disableTotpSubmitting={false} /> ); } @@ -1288,21 +1296,25 @@ export default function App() { onRememberDeviceChange={setRememberDevice} onConfirmTotp={() => void handleTotpVerify()} onCancelTotp={() => { + if (totpSubmitting) return; setPendingTotp(null); setTotpCode(''); setRememberDevice(true); }} onUseRecoveryCode={() => { + if (totpSubmitting) return; setPendingTotp(null); setTotpCode(''); setRememberDevice(true); navigate('/recover-2fa'); }} + totpSubmitting={totpSubmitting} disableTotpOpen={false} disableTotpPassword="" onDisableTotpPasswordChange={() => {}} onConfirmDisableTotp={() => {}} onCancelDisableTotp={() => {}} + disableTotpSubmitting={false} /> ); @@ -1341,14 +1353,27 @@ export default function App() { onConfirmTotp={() => {}} onCancelTotp={() => {}} onUseRecoveryCode={() => {}} + totpSubmitting={false} disableTotpOpen={disableTotpOpen} disableTotpPassword={disableTotpPassword} onDisableTotpPasswordChange={setDisableTotpPassword} - onConfirmDisableTotp={() => void accountSecurityActions.disableTotp()} + onConfirmDisableTotp={() => { + if (disableTotpSubmitting) return; + void (async () => { + setDisableTotpSubmitting(true); + try { + await accountSecurityActions.disableTotp(); + } finally { + setDisableTotpSubmitting(false); + } + })(); + }} onCancelDisableTotp={() => { + if (disableTotpSubmitting) return; setDisableTotpOpen(false); setDisableTotpPassword(''); }} + disableTotpSubmitting={disableTotpSubmitting} /> ); diff --git a/webapp/src/components/AppGlobalOverlays.tsx b/webapp/src/components/AppGlobalOverlays.tsx index 8f8940e..e6f8ab8 100644 --- a/webapp/src/components/AppGlobalOverlays.tsx +++ b/webapp/src/components/AppGlobalOverlays.tsx @@ -27,11 +27,13 @@ interface AppGlobalOverlaysProps { onConfirmTotp: () => void; onCancelTotp: () => void; onUseRecoveryCode: () => void; + totpSubmitting: boolean; disableTotpOpen: boolean; disableTotpPassword: string; onDisableTotpPasswordChange: (value: string) => void; onConfirmDisableTotp: () => void; onCancelDisableTotp: () => void; + disableTotpSubmitting: boolean; } export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) { @@ -57,12 +59,14 @@ export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) { confirmText={t('txt_verify')} cancelText={t('txt_cancel')} showIcon={false} + confirmDisabled={props.totpSubmitting} + cancelDisabled={props.totpSubmitting} onConfirm={props.onConfirmTotp} onCancel={props.onCancelTotp} afterActions={(
-
@@ -86,6 +90,8 @@ export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) { cancelText={t('txt_cancel')} danger showIcon={false} + confirmDisabled={props.disableTotpSubmitting} + cancelDisabled={props.disableTotpSubmitting} onConfirm={props.onConfirmDisableTotp} onCancel={props.onCancelDisableTotp} > diff --git a/webapp/src/components/BackupCenterPage.tsx b/webapp/src/components/BackupCenterPage.tsx index e57e17b..3c9f92c 100644 --- a/webapp/src/components/BackupCenterPage.tsx +++ b/webapp/src/components/BackupCenterPage.tsx @@ -528,6 +528,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { allowChecksumMismatch: boolean = false, knownIntegrity?: BackupFileIntegrityCheckResult ) { + if (importing) return; if (!selectedFile) { const message = t('txt_backup_file_required'); setLocalError(message); @@ -654,6 +655,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { } async function handleDeleteRemote(path: string) { + if (deletingRemotePath) return; if (!savedSelectedDestination) return; setDeletingRemotePath(path); setLocalError(''); @@ -723,6 +725,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { allowChecksumMismatch: boolean = false, knownIntegrity?: BackupFileIntegrityCheckResult ) { + if (restoringRemotePath) return; if (!savedSelectedDestination) return; setConfirmRemoteReplaceOpen(false); setConfirmIntegrityWarningOpen(false); @@ -896,9 +899,12 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { message={selectedFile ? t('txt_backup_selected_file_name', { name: selectedFile.name }) : t('txt_backup_restore_note')} confirmText={t('txt_backup_import')} cancelText={t('txt_cancel')} + confirmDisabled={importing} + cancelDisabled={importing} danger onConfirm={() => void runLocalRestore(false)} onCancel={() => { + if (importing) return; setConfirmLocalRestoreOpen(false); resetSelectedFile(); resetPendingIntegrityWarning(); @@ -959,6 +965,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { variant="warning" confirmText={t('txt_backup_restore_checksum_warning_confirm')} cancelText={t('txt_cancel')} + confirmDisabled={importing || !!restoringRemotePath} + cancelDisabled={importing || !!restoringRemotePath} danger onConfirm={() => { if (!pendingRestoreIntegrity) return; @@ -984,6 +992,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { message={t('txt_backup_remote_delete_confirm_message', { name: pendingRemoteDeletePath.split('/').pop() || pendingRemoteDeletePath })} confirmText={t('txt_delete')} cancelText={t('txt_cancel')} + confirmDisabled={!!deletingRemotePath} + cancelDisabled={!!deletingRemotePath} danger onConfirm={() => void handleDeleteRemote(pendingRemoteDeletePath)} onCancel={() => { @@ -1001,6 +1011,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { })} confirmText={t('txt_delete')} cancelText={t('txt_cancel')} + confirmDisabled={savingSettings} + cancelDisabled={savingSettings} danger onConfirm={() => void handleDeleteDestination()} onCancel={() => { diff --git a/webapp/src/components/ImportPage.tsx b/webapp/src/components/ImportPage.tsx index 173ce12..2c66bf6 100644 --- a/webapp/src/components/ImportPage.tsx +++ b/webapp/src/components/ImportPage.tsx @@ -468,6 +468,7 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys } async function handlePasswordImportConfirm() { + if (isPasswordSubmitting) return; if (!pendingPasswordImport) return; setIsPasswordSubmitting(true); try { @@ -486,6 +487,7 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys } async function handleZipPasswordImportConfirm() { + if (isZipPasswordSubmitting) return; if (!pendingZipFile) return; setIsZipPasswordSubmitting(true); try { @@ -558,6 +560,7 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys } async function handleExportConfirmPassword() { + if (isExporting) return; const masterPassword = String(exportAuthPassword || '').trim(); if (!masterPassword) { onNotify('error', t('txt_master_password_is_required')); @@ -736,6 +739,8 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys confirmText={isExporting ? t('txt_loading') : t('txt_verify')} cancelText={t('txt_cancel')} showIcon={false} + confirmDisabled={isExporting} + cancelDisabled={isExporting} onConfirm={() => void handleExportConfirmPassword()} onCancel={() => { if (isExporting) return; @@ -761,6 +766,8 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys confirmText={isPasswordSubmitting ? t('txt_loading') : t('txt_import')} cancelText={t('txt_cancel')} showIcon={false} + confirmDisabled={isPasswordSubmitting} + cancelDisabled={isPasswordSubmitting} onConfirm={() => void handlePasswordImportConfirm()} onCancel={() => { if (isPasswordSubmitting) return; @@ -787,6 +794,8 @@ export default function ImportPage({ onImport, onImportEncryptedRaw, accountKeys confirmText={isZipPasswordSubmitting ? t('txt_loading') : t('txt_import')} cancelText={t('txt_cancel')} showIcon={false} + confirmDisabled={isZipPasswordSubmitting} + cancelDisabled={isZipPasswordSubmitting} onConfirm={() => void handleZipPasswordImportConfirm()} onCancel={() => { if (isZipPasswordSubmitting) return; diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index 3ec57dd..0cb6e91 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -1009,6 +1009,7 @@ function folderName(id: string | null | undefined): string {
@@ -118,11 +121,22 @@ export default function VaultDialogs(props: VaultDialogsProps) { message={t('txt_archive_selected_items_message', { count: props.selectedCount })} confirmText={t('txt_archive')} cancelText={t('txt_cancel')} + confirmDisabled={props.busy} + cancelDisabled={props.busy} onConfirm={props.onConfirmBulkArchive} onCancel={props.onCancelBulkArchive} /> - + - + - +