From 685a1138e4d5d22f988b2846344d071cdac14163 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 15 Nov 2025 23:47:35 +0800 Subject: [PATCH] refactor(mcp): complete form refactoring for unified MCP management Complete the v3.7.0 MCP refactoring by updating the form layer to match the unified architecture already implemented in data/service/API layers. **Breaking Changes:** - Remove confusing `appId` parameter from McpFormModal - Replace with `defaultFormat` (json/toml) and `defaultEnabledApps` (array) **Form Enhancements:** - Add app enablement checkboxes (Claude/Codex/Gemini) directly in the form - Smart defaults: new servers default to Claude enabled, editing preserves state - Support "draft" mode: servers can be created without enabling any apps **Architecture Improvements:** - Eliminate semantic confusion: format selection separate from app targeting - One-step workflow: configure and enable apps in single form submission - Consistent with unified backend: `apps: { claude, codex, gemini }` **Testing:** - Update test mocks to use `useUpsertMcpServer` hook - Add test case for creating servers with no apps enabled - Fix parameter references from `appId` to `defaultFormat` **i18n:** - Add `mcp.form.enabledApps` translation (zh/en) - Add `mcp.form.noAppsWarning` translation (zh/en) This completes the MCP management refactoring, ensuring all layers (data, service, API, UI) follow the same unified architecture pattern. --- src/components/mcp/McpFormModal.tsx | 117 +++++++++++++--- src/components/mcp/UnifiedMcpPanel.tsx | 3 +- src/i18n/locales/en.json | 2 + src/i18n/locales/zh.json | 2 + tests/components/McpFormModal.test.tsx | 180 +++++++++++++++---------- 5 files changed, 210 insertions(+), 94 deletions(-) diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 32918d7..fbaf12f 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Save, Plus, AlertCircle, ChevronDown, ChevronUp } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -30,26 +31,28 @@ import { useMcpValidation } from "./useMcpValidation"; import { useUpsertMcpServer } from "@/hooks/useMcp"; interface McpFormModalProps { - appId: AppId; editingId?: string; initialData?: McpServer; onSave: () => Promise; // v3.7.0: 简化为仅用于关闭表单的回调 onClose: () => void; existingIds?: string[]; + defaultFormat?: "json" | "toml"; // 默认配置格式(可选,默认为 JSON) + defaultEnabledApps?: AppId[]; // 默认启用到哪些应用(可选,默认为 Claude) } /** - * MCP 表单模态框组件(简化版) - * Claude: 使用 JSON 格式 - * Codex: 使用 TOML 格式 + * MCP 表单模态框组件(v3.7.0 完整重构版) + * - 支持 JSON 和 TOML 两种格式 + * - 统一管理,通过复选框选择启用到哪些应用 */ const McpFormModal: React.FC = ({ - appId, editingId, initialData, onSave, onClose, existingIds = [], + defaultFormat = "json", + defaultEnabledApps = ["claude"], }) => { const { t } = useTranslation(); const { formatTomlError, validateTomlConfig, validateJsonConfig } = @@ -68,6 +71,23 @@ const McpFormModal: React.FC = ({ const [formDocs, setFormDocs] = useState(initialData?.docs || ""); const [formTags, setFormTags] = useState(initialData?.tags?.join(", ") || ""); + // 启用状态:编辑模式使用现有值,新增模式使用默认值 + const [enabledApps, setEnabledApps] = useState<{ + claude: boolean; + codex: boolean; + gemini: boolean; + }>(() => { + if (initialData?.apps) { + return { ...initialData.apps }; + } + // 新增模式:根据 defaultEnabledApps 设置初始值 + return { + claude: defaultEnabledApps.includes("claude"), + codex: defaultEnabledApps.includes("codex"), + gemini: defaultEnabledApps.includes("gemini"), + }; + }); + // 编辑模式下禁止修改 ID const isEditing = !!editingId; @@ -84,11 +104,20 @@ const McpFormModal: React.FC = ({ isEditing ? hasAdditionalInfo : false, ); - // 根据 appId 决定初始格式 + // 配置格式:优先使用 defaultFormat,编辑模式下可从现有数据推断 + const useTomlFormat = useMemo(() => { + if (initialData?.server) { + // 编辑模式:尝试从现有数据推断格式(这里简化处理,默认 JSON) + return defaultFormat === "toml"; + } + return defaultFormat === "toml"; + }, [defaultFormat, initialData]); + + // 根据格式决定初始配置 const [formConfig, setFormConfig] = useState(() => { const spec = initialData?.server; if (!spec) return ""; - if (appId === "codex") { + if (useTomlFormat) { return mcpServerToToml(spec); } return JSON.stringify(spec, null, 2); @@ -99,8 +128,8 @@ const McpFormModal: React.FC = ({ const [isWizardOpen, setIsWizardOpen] = useState(false); const [idError, setIdError] = useState(""); - // 判断是否使用 TOML 格式 - const useToml = appId === "codex"; + // 判断是否使用 TOML 格式(向后兼容,后续可扩展为格式切换按钮) + const useToml = useTomlFormat; const wizardInitialSpec = useMemo(() => { const fallback = initialData?.server; @@ -333,12 +362,8 @@ const McpFormModal: React.FC = ({ id: trimmedId, name: finalName, server: serverSpec, - // 确保 apps 字段始终存在(v3.7.0 新架构必需) - apps: initialData?.apps || { - claude: false, - codex: false, - gemini: false, - }, + // 使用表单中的启用状态(v3.7.0 完整重构) + apps: enabledApps, }; const descriptionTrimmed = formDescription.trim(); @@ -387,11 +412,7 @@ const McpFormModal: React.FC = ({ }; const getFormTitle = () => { - if (appId === "claude") { - return isEditing ? t("mcp.editClaudeServer") : t("mcp.addClaudeServer"); - } else { - return isEditing ? t("mcp.editCodexServer") : t("mcp.addCodexServer"); - } + return isEditing ? t("mcp.editServer") : t("mcp.addServer"); }; return ( @@ -477,6 +498,62 @@ const McpFormModal: React.FC = ({ /> + {/* 启用到哪些应用(v3.7.0 新增) */} +
+ +
+
+ + setEnabledApps({ ...enabledApps, claude: checked }) + } + /> + +
+ +
+ + setEnabledApps({ ...enabledApps, codex: checked }) + } + /> + +
+ +
+ + setEnabledApps({ ...enabledApps, gemini: checked }) + } + /> + +
+
+
+ {/* 可折叠的附加信息按钮 */}