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.
This commit is contained in:
Jason
2025-11-15 23:47:35 +08:00
parent 154ff4c819
commit 685a1138e4
5 changed files with 210 additions and 94 deletions

View File

@@ -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<void>; // 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<McpFormModalProps> = ({
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<McpFormModalProps> = ({
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<McpFormModalProps> = ({
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<McpFormModalProps> = ({
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<McpFormModalProps> = ({
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<McpFormModalProps> = ({
};
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<McpFormModalProps> = ({
/>
</div>
{/* 启用到哪些应用v3.7.0 新增) */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("mcp.form.enabledApps")}
</label>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="enable-claude"
checked={enabledApps.claude}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, claude: checked })
}
/>
<label
htmlFor="enable-claude"
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.claude")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="enable-codex"
checked={enabledApps.codex}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, codex: checked })
}
/>
<label
htmlFor="enable-codex"
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.codex")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="enable-gemini"
checked={enabledApps.gemini}
onCheckedChange={(checked: boolean) =>
setEnabledApps({ ...enabledApps, gemini: checked })
}
/>
<label
htmlFor="enable-gemini"
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
>
{t("mcp.unifiedPanel.apps.gemini")}
</label>
</div>
</div>
</div>
{/* 可折叠的附加信息按钮 */}
<div>
<button