From 3b500be525072d4bdfe223d4a83dbeb8c23a4d6b Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 24 Nov 2025 22:44:02 +0800 Subject: [PATCH] feat(frontend): update DeepLinkImportDialog for multi-resource imports Refactor the main deeplink import dialog to handle all resource types with proper confirmation UI and post-import actions. Key changes: - Add resource-specific confirmation components (Prompt/MCP/Skill) - Implement typed result handling with discriminated unions - Add MCP result type guard for backward compatibility - Implement resource-specific cache invalidation strategies - Add custom event dispatching for non-React-Query resources Import flow improvements: - Provider: Invalidate provider queries, show success toast - Prompt: Dispatch "prompt-imported" event, trigger manual refresh - MCP: Aggressive cache invalidation with refetchQueries, handle partial success (show warning if some imports failed) - Skill: Force refetch skills query with refetchType: "all" Error handling: - Graceful fallback for legacy backend responses (no type field) - MCP-specific result detection via type guard - Detailed error messages in toasts UI improvements: - Dynamic dialog title based on resource type - Resource-specific confirmation content rendering - Better visual feedback during import process --- src/components/DeepLinkImportDialog.tsx | 603 +++++++++++++++--------- 1 file changed, 374 insertions(+), 229 deletions(-) diff --git a/src/components/DeepLinkImportDialog.tsx b/src/components/DeepLinkImportDialog.tsx index 3cd3e75a5..97e4c213c 100644 --- a/src/components/DeepLinkImportDialog.tsx +++ b/src/components/DeepLinkImportDialog.tsx @@ -13,6 +13,10 @@ import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; +import { PromptConfirmation } from "./deeplink/PromptConfirmation"; +import { McpConfirmation } from "./deeplink/McpConfirmation"; +import { SkillConfirmation } from "./deeplink/SkillConfirmation"; +import { ProviderIcon } from "./ProviderIcon"; interface DeeplinkError { url: string; @@ -26,6 +30,24 @@ export function DeepLinkImportDialog() { const [isImporting, setIsImporting] = useState(false); const [isOpen, setIsOpen] = useState(false); + // 容错判断:MCP 导入结果可能缺少 type 字段 + const isMcpImportResult = ( + value: unknown, + ): value is { + importedCount: number; + importedIds: string[]; + failed: Array<{ id: string; error: string }>; + type?: "mcp"; + } => { + if (!value || typeof value !== "object") return false; + const v = value as Record; + return ( + typeof v.importedCount === "number" && + Array.isArray(v.importedIds) && + Array.isArray(v.failed) + ); + }; + useEffect(() => { // Listen for deep link import events const unlistenImport = listen( @@ -78,22 +100,89 @@ export function DeepLinkImportDialog() { setIsImporting(true); try { - await deeplinkApi.importFromDeeplink(request); + const result = await deeplinkApi.importFromDeeplink(request); + const refreshMcp = async (summary: { + importedCount: number; + importedIds: string[]; + failed: Array<{ id: string; error: string }>; + }) => { + // 强制刷新 MCP 相关缓存,确保管理页重新从数据库加载 + await queryClient.invalidateQueries({ + queryKey: ["mcp", "all"], + refetchType: "all", + }); + await queryClient.refetchQueries({ + queryKey: ["mcp", "all"], + type: "all", + }); - // Invalidate provider queries to refresh the list - await queryClient.invalidateQueries({ - queryKey: ["providers", request.app], - }); + if (summary.failed.length > 0) { + toast.warning(`部分导入成功`, { + description: `成功: ${summary.importedCount}, 失败: ${summary.failed.length}`, + }); + } else { + toast.success("MCP Servers 导入成功", { + description: `成功导入 ${summary.importedCount} 个服务器`, + }); + } + }; - toast.success(t("deeplink.importSuccess"), { - description: t("deeplink.importSuccessDescription", { - name: request.name, - }), - }); + // Handle different result types + if ("type" in result) { + if (result.type === "provider") { + await queryClient.invalidateQueries({ + queryKey: ["providers", request.app], + }); + toast.success(t("deeplink.importSuccess"), { + description: t("deeplink.importSuccessDescription", { + name: request.name, + }), + }); + } else if (result.type === "prompt") { + // Prompts don't use React Query, trigger a custom event for refresh + window.dispatchEvent( + new CustomEvent("prompt-imported", { + detail: { app: request.app }, + }), + ); + toast.success("提示词导入成功", { + description: `已导入提示词: ${request.name}`, + }); + } else if (result.type === "mcp") { + await refreshMcp(result); + } else if (result.type === "skill") { + // Refresh Skills with aggressive strategy + queryClient.invalidateQueries({ + queryKey: ["skills"], + refetchType: "all", + }); + await queryClient.refetchQueries({ + queryKey: ["skills"], + type: "all", + }); + toast.success("Skill 仓库添加成功", { + description: `已添加仓库: ${request.repo}`, + }); + } + } else if (isMcpImportResult(result)) { + // 兜底处理:旧版本后端可能未返回 type 字段 + await refreshMcp(result); + } else { + // Legacy return type (string ID) - assume provider + await queryClient.invalidateQueries({ + queryKey: ["providers", request.app], + }); + toast.success(t("deeplink.importSuccess"), { + description: t("deeplink.importSuccessDescription", { + name: request.name, + }), + }); + } + // Close dialog after all refreshes complete setIsOpen(false); } catch (error) { - console.error("Failed to import provider from deep link:", error); + console.error("Failed to import from deep link:", error); toast.error(t("deeplink.importError"), { description: error instanceof Error ? error.message : String(error), }); @@ -189,6 +278,34 @@ export function DeepLinkImportDialog() { return value; }; + const getTitle = () => { + if (!request) return t("deeplink.confirmImport"); + switch (request.resource) { + case "prompt": + return "导入提示词"; + case "mcp": + return "导入 MCP Servers"; + case "skill": + return "添加 Skill 仓库"; + default: + return t("deeplink.confirmImport"); + } + }; + + const getDescription = () => { + if (!request) return t("deeplink.confirmImportDescription"); + switch (request.resource) { + case "prompt": + return "请确认是否导入此系统提示词"; + case "mcp": + return "请确认是否导入这些 MCP Servers"; + case "skill": + return "请确认是否添加此 Skill 仓库"; + default: + return t("deeplink.confirmImportDescription"); + } + }; + return ( @@ -196,200 +313,197 @@ export function DeepLinkImportDialog() { <> {/* 标题显式左对齐,避免默认居中样式影响 */} - {t("deeplink.confirmImport")} - - {t("deeplink.confirmImportDescription")} - + {getTitle()} + {getDescription()} {/* 主体内容整体右移,略大于标题内边距,让内容看起来不贴边 */}
- {/* App Type */} -
-
- {t("deeplink.app")} -
-
- {request.app} -
-
- - {/* Provider Name */} -
-
- {t("deeplink.providerName")} -
-
- {request.name} -
-
- - {/* Homepage */} -
-
- {t("deeplink.homepage")} -
-
- {request.homepage} -
-
- - {/* API Endpoint */} -
-
- {t("deeplink.endpoint")} -
-
- {request.endpoint} -
-
- - {/* API Key (masked) */} -
-
- {t("deeplink.apiKey")} -
-
- {maskedApiKey} -
-
- - {/* Model Fields - 根据应用类型显示不同的模型字段 */} - {request.app === "claude" ? ( - <> - {/* Claude 四种模型字段 */} - {request.haikuModel && ( -
-
- {t("deeplink.haikuModel")} -
-
- {request.haikuModel} -
-
- )} - {request.sonnetModel && ( -
-
- {t("deeplink.sonnetModel")} -
-
- {request.sonnetModel} -
-
- )} - {request.opusModel && ( -
-
- {t("deeplink.opusModel")} -
-
- {request.opusModel} -
-
- )} - {request.model && ( -
-
- {t("deeplink.multiModel")} -
-
- {request.model} -
-
- )} - - ) : ( - <> - {/* Codex 和 Gemini 使用通用 model 字段 */} - {request.model && ( -
-
- {t("deeplink.model")} -
-
- {request.model} -
-
- )} - + {request.resource === "prompt" && ( + + )} + {request.resource === "mcp" && ( + + )} + {request.resource === "skill" && ( + )} - {/* Notes (if present) */} - {request.notes && ( -
-
- {t("deeplink.notes")} -
-
- {request.notes} -
-
- )} + {/* Legacy Provider View */} + {(request.resource === "provider" || !request.resource) && ( + <> + {/* Provider Icon - enlarge and center near the top */} + {request.icon && ( +
+ +
+ )} - {/* Config File Details (v3.8+) */} - {hasConfigFile && ( -
+ {/* App Type */}
- {t("deeplink.configSource")} + {t("deeplink.app")}
-
- - {configSource === "base64" - ? t("deeplink.configEmbedded") - : t("deeplink.configRemote")} - - {request.configFormat && ( - - {request.configFormat} - - )} +
+ {request.app}
- {/* Parsed Config Details */} - {parsedConfig && ( -
-
- {t("deeplink.configDetails")} -
+ {/* Provider Name */} +
+
+ {t("deeplink.providerName")} +
+
+ {request.name} +
+
- {/* Claude config */} - {parsedConfig.type === "claude" && parsedConfig.env && ( -
- {Object.entries(parsedConfig.env).map( - ([key, value]) => ( -
- - {key} - - - {maskValue(key, String(value))} - -
- ), - )} + {/* Homepage */} +
+
+ {t("deeplink.homepage")} +
+
+ {request.homepage} +
+
+ + {/* API Endpoint */} +
+
+ {t("deeplink.endpoint")} +
+
+ {request.endpoint} +
+
+ + {/* API Key (masked) */} +
+
+ {t("deeplink.apiKey")} +
+
+ {maskedApiKey} +
+
+ + {/* Model Fields - 根据应用类型显示不同的模型字段 */} + {request.app === "claude" ? ( + <> + {/* Claude 四种模型字段 */} + {request.haikuModel && ( +
+
+ {t("deeplink.haikuModel")} +
+
+ {request.haikuModel} +
)} + {request.sonnetModel && ( +
+
+ {t("deeplink.sonnetModel")} +
+
+ {request.sonnetModel} +
+
+ )} + {request.opusModel && ( +
+
+ {t("deeplink.opusModel")} +
+
+ {request.opusModel} +
+
+ )} + {request.model && ( +
+
+ {t("deeplink.multiModel")} +
+
+ {request.model} +
+
+ )} + + ) : ( + <> + {/* Codex 和 Gemini 使用通用 model 字段 */} + {request.model && ( +
+
+ {t("deeplink.model")} +
+
+ {request.model} +
+
+ )} + + )} - {/* Codex config */} - {parsedConfig.type === "codex" && ( -
- {parsedConfig.auth && - Object.keys(parsedConfig.auth).length > 0 && ( + {/* Notes (if present) */} + {request.notes && ( +
+
+ {t("deeplink.notes")} +
+
+ {request.notes} +
+
+ )} + + {/* Config File Details (v3.8+) */} + {hasConfigFile && ( +
+
+
+ {t("deeplink.configSource")} +
+
+ + {configSource === "base64" + ? t("deeplink.configEmbedded") + : t("deeplink.configRemote")} + + {request.configFormat && ( + + {request.configFormat} + + )} +
+
+ + {/* Parsed Config Details */} + {parsedConfig && ( +
+
+ {t("deeplink.configDetails")} +
+ + {/* Claude config */} + {parsedConfig.type === "claude" && + parsedConfig.env && (
-
- Auth: -
- {Object.entries(parsedConfig.auth).map( + {Object.entries(parsedConfig.env).map( ([key, value]) => (
{key} @@ -402,61 +516,92 @@ export function DeepLinkImportDialog() { )}
)} - {parsedConfig.tomlConfig && ( -
-
- TOML Config: -
-
-                                {parsedConfig.tomlConfig.substring(0, 300)}
-                                {parsedConfig.tomlConfig.length > 300 && "..."}
-                              
+ + {/* Codex config */} + {parsedConfig.type === "codex" && ( +
+ {parsedConfig.auth && + Object.keys(parsedConfig.auth).length > 0 && ( +
+
+ Auth: +
+ {Object.entries(parsedConfig.auth).map( + ([key, value]) => ( +
+ + {key} + + + {maskValue(key, String(value))} + +
+ ), + )} +
+ )} + {parsedConfig.tomlConfig && ( +
+
+ TOML Config: +
+
+                                    {parsedConfig.tomlConfig.substring(0, 300)}
+                                    {parsedConfig.tomlConfig.length > 300 &&
+                                      "..."}
+                                  
+
+ )}
)} -
- )} - {/* Gemini config */} - {parsedConfig.type === "gemini" && parsedConfig.env && ( -
- {Object.entries(parsedConfig.env).map( - ([key, value]) => ( -
- - {key} - - - {maskValue(key, String(value))} - + {/* Gemini config */} + {parsedConfig.type === "gemini" && + parsedConfig.env && ( +
+ {Object.entries(parsedConfig.env).map( + ([key, value]) => ( +
+ + {key} + + + {maskValue(key, String(value))} + +
+ ), + )}
- ), - )} + )} +
+ )} + + {/* Config URL (if remote) */} + {request.configUrl && ( +
+
+ {t("deeplink.configUrl")} +
+
+ {request.configUrl} +
)}
)} - {/* Config URL (if remote) */} - {request.configUrl && ( -
-
- {t("deeplink.configUrl")} -
-
- {request.configUrl} -
-
- )} -
+ {/* Warning */} +
+ {t("deeplink.warning")} +
+ )} - - {/* Warning */} -
- {t("deeplink.warning")} -