fix(import): refresh all providers immediately after SQL import

- Remove setTimeout delay that could be cancelled on component unmount
- Invalidate all providers cache (not just current app) since import affects all apps
- Call onImportSuccess before sync to ensure UI refresh even if sync fails
- Update i18n: "Data refreshed" (past tense, reflecting immediate action)
This commit is contained in:
Jason
2025-12-19 11:18:22 +08:00
parent 8ce6edfe94
commit dffaf4071d
6 changed files with 24 additions and 24 deletions
+16 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useQueryClient } from "@tanstack/react-query";
import { import {
Plus, Plus,
Settings, Settings,
@@ -45,6 +46,7 @@ type View = "providers" | "settings" | "prompts" | "skills" | "mcp" | "agents";
function App() { function App() {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient();
const [activeApp, setActiveApp] = useState<AppId>("claude"); const [activeApp, setActiveApp] = useState<AppId>("claude");
const [currentView, setCurrentView] = useState<View>("providers"); const [currentView, setCurrentView] = useState<View>("providers");
@@ -258,7 +260,20 @@ function App() {
// 导入配置成功后刷新 // 导入配置成功后刷新
const handleImportSuccess = async () => { const handleImportSuccess = async () => {
await refetch(); try {
// 导入会影响所有应用的供应商数据:刷新所有 providers 缓存
await queryClient.invalidateQueries({
queryKey: ["providers"],
refetchType: "all",
});
await queryClient.refetchQueries({
queryKey: ["providers"],
type: "all",
});
} catch (error) {
console.error("[App] Failed to refresh providers after import", error);
await refetch();
}
try { try {
await providersApi.updateTrayMenu(); await providersApi.updateTrayMenu();
} catch (error) { } catch (error) {
+5 -14
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { settingsApi } from "@/lib/api"; import { settingsApi } from "@/lib/api";
@@ -39,15 +39,6 @@ export function useImportExport(
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [backupId, setBackupId] = useState<string | null>(null); const [backupId, setBackupId] = useState<string | null>(null);
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const successTimerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (successTimerRef.current) {
window.clearTimeout(successTimerRef.current);
}
};
}, []);
const clearSelection = useCallback(() => { const clearSelection = useCallback(() => {
setSelectedFile(""); setSelectedFile("");
@@ -105,6 +96,10 @@ export function useImportExport(
} }
setBackupId(result.backupId ?? null); setBackupId(result.backupId ?? null);
// 导入成功后立即触发外部刷新(与 live 同步结果解耦)
// - 避免 sync 失败时 UI 不刷新
// - 避免依赖 setTimeout(组件卸载会取消)
void onImportSuccess?.();
const syncResult = await syncCurrentProvidersLiveSafe(); const syncResult = await syncCurrentProvidersLiveSafe();
if (syncResult.ok) { if (syncResult.ok) {
@@ -114,10 +109,6 @@ export function useImportExport(
defaultValue: "配置导入成功", defaultValue: "配置导入成功",
}), }),
); );
successTimerRef.current = window.setTimeout(() => {
void onImportSuccess?.();
}, 1500);
} else { } else {
console.error( console.error(
"[useImportExport] Failed to sync live config", "[useImportExport] Failed to sync live config",
+1 -1
View File
@@ -162,7 +162,7 @@
"selectFileFailed": "Please choose a valid SQL backup file", "selectFileFailed": "Please choose a valid SQL backup file",
"configCorrupted": "SQL file may be corrupted or invalid", "configCorrupted": "SQL file may be corrupted or invalid",
"backupId": "Backup ID", "backupId": "Backup ID",
"autoReload": "Data will refresh automatically in 2 seconds...", "autoReload": "Data refreshed",
"languageOptionChinese": "中文", "languageOptionChinese": "中文",
"languageOptionEnglish": "English", "languageOptionEnglish": "English",
"languageOptionJapanese": "日本語", "languageOptionJapanese": "日本語",
+1 -1
View File
@@ -162,7 +162,7 @@
"selectFileFailed": "有効な SQL バックアップファイルを選択してください", "selectFileFailed": "有効な SQL バックアップファイルを選択してください",
"configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります", "configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります",
"backupId": "バックアップ ID", "backupId": "バックアップ ID",
"autoReload": "2 秒後に自動で再読み込みします...", "autoReload": "データを更新しました",
"languageOptionChinese": "中文", "languageOptionChinese": "中文",
"languageOptionEnglish": "English", "languageOptionEnglish": "English",
"languageOptionJapanese": "日本語", "languageOptionJapanese": "日本語",
+1 -1
View File
@@ -162,7 +162,7 @@
"selectFileFailed": "请选择有效的 SQL 备份文件", "selectFileFailed": "请选择有效的 SQL 备份文件",
"configCorrupted": "SQL 文件可能已损坏或格式不正确", "configCorrupted": "SQL 文件可能已损坏或格式不正确",
"backupId": "备份ID", "backupId": "备份ID",
"autoReload": "数据将在2秒后自动刷新...", "autoReload": "数据已刷新",
"languageOptionChinese": "中文", "languageOptionChinese": "中文",
"languageOptionEnglish": "English", "languageOptionEnglish": "English",
"languageOptionJapanese": "日本語", "languageOptionJapanese": "日本語",
-6
View File
@@ -110,12 +110,6 @@ describe("useImportExport Hook", () => {
expect(result.current.status).toBe("success"); expect(result.current.status).toBe("success");
expect(result.current.backupId).toBe("backup-123"); expect(result.current.backupId).toBe("backup-123");
expect(toastSuccessMock).toHaveBeenCalledTimes(1); expect(toastSuccessMock).toHaveBeenCalledTimes(1);
// Skip delay to execute callback
await act(async () => {
vi.runOnlyPendingTimers();
});
expect(onImportSuccess).toHaveBeenCalledTimes(1); expect(onImportSuccess).toHaveBeenCalledTimes(1);
}); });