feat(mcp): add overwrite warning and improve sync option visibility
- Move sync checkbox to footer alongside save/cancel buttons for better visibility
- Add real-time conflict detection to check for duplicate MCP IDs on other side
- Display amber warning icon when sync would overwrite existing config
- Add i18n keys for overwrite warning (zh: "将覆盖 {{target}} 中的同名配置")
- Update UI layout to use justify-between for better spacing
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { X, Save, AlertCircle, ChevronDown, ChevronUp } from "lucide-react";
|
import { X, Save, AlertCircle, ChevronDown, ChevronUp, AlertTriangle } from "lucide-react";
|
||||||
import { McpServer, McpServerSpec } from "../../types";
|
import { McpServer, McpServerSpec } from "../../types";
|
||||||
import {
|
import {
|
||||||
mcpPresets,
|
mcpPresets,
|
||||||
@@ -118,16 +118,40 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const [idError, setIdError] = useState("");
|
const [idError, setIdError] = useState("");
|
||||||
const [syncOtherSide, setSyncOtherSide] = useState(false);
|
const [syncOtherSide, setSyncOtherSide] = useState(false);
|
||||||
|
const [otherSideHasConflict, setOtherSideHasConflict] = useState(false);
|
||||||
|
|
||||||
// 判断是否使用 TOML 格式
|
// 判断是否使用 TOML 格式
|
||||||
const useToml = appType === "codex";
|
const useToml = appType === "codex";
|
||||||
const syncTargetLabel =
|
const syncTargetLabel =
|
||||||
appType === "claude" ? t("apps.codex") : t("apps.claude");
|
appType === "claude" ? t("apps.codex") : t("apps.claude");
|
||||||
|
const otherAppType: AppType = appType === "claude" ? "codex" : "claude";
|
||||||
const syncCheckboxId = useMemo(
|
const syncCheckboxId = useMemo(
|
||||||
() => `sync-other-side-${appType}`,
|
() => `sync-other-side-${appType}`,
|
||||||
[appType],
|
[appType],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 检测另一侧是否有同名 MCP
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOtherSide = async () => {
|
||||||
|
const currentId = formId.trim();
|
||||||
|
if (!currentId) {
|
||||||
|
setOtherSideHasConflict(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const otherConfig = await window.api.getMcpConfig(otherAppType);
|
||||||
|
const hasConflict = Object.keys(otherConfig.servers || {}).includes(currentId);
|
||||||
|
setOtherSideHasConflict(hasConflict);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查另一侧 MCP 配置失败:", error);
|
||||||
|
setOtherSideHasConflict(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOtherSide();
|
||||||
|
}, [formId, otherAppType]);
|
||||||
|
|
||||||
const wizardInitialSpec = useMemo(() => {
|
const wizardInitialSpec = useMemo(() => {
|
||||||
const fallback = initialData?.server;
|
const fallback = initialData?.server;
|
||||||
if (!formConfig.trim()) {
|
if (!formConfig.trim()) {
|
||||||
@@ -666,50 +690,59 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 双端同步选项 */}
|
|
||||||
<div className="mt-4 flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40">
|
|
||||||
<input
|
|
||||||
id={syncCheckboxId}
|
|
||||||
type="checkbox"
|
|
||||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
|
|
||||||
checked={syncOtherSide}
|
|
||||||
onChange={(event) => setSyncOtherSide(event.target.checked)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={syncCheckboxId}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<span className="font-medium">
|
|
||||||
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
|
|
||||||
</span>
|
|
||||||
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex-shrink-0 flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
<div className="flex-shrink-0 flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
||||||
<button
|
{/* 双端同步选项 */}
|
||||||
onClick={onClose}
|
<div className="flex items-center gap-3">
|
||||||
className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium"
|
<div className="flex items-center gap-2">
|
||||||
>
|
<input
|
||||||
{t("common.cancel")}
|
id={syncCheckboxId}
|
||||||
</button>
|
type="checkbox"
|
||||||
<button
|
className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800"
|
||||||
onClick={handleSubmit}
|
checked={syncOtherSide}
|
||||||
disabled={saving || (!isEditing && !!idError)}
|
onChange={(event) => setSyncOtherSide(event.target.checked)}
|
||||||
className={`inline-flex items-center gap-2 ${buttonStyles.mcp}`}
|
/>
|
||||||
>
|
<label
|
||||||
<Save size={16} />
|
htmlFor={syncCheckboxId}
|
||||||
{saving
|
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
|
||||||
? t("common.saving")
|
title={t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
|
||||||
: isEditing
|
>
|
||||||
? t("common.save")
|
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
|
||||||
: t("common.add")}
|
</label>
|
||||||
</button>
|
</div>
|
||||||
|
{syncOtherSide && otherSideHasConflict && (
|
||||||
|
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{t("mcp.form.willOverwriteWarning", { target: syncTargetLabel })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={saving || (!isEditing && !!idError)}
|
||||||
|
className={`inline-flex items-center gap-2 ${buttonStyles.mcp}`}
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
{saving
|
||||||
|
? t("common.saving")
|
||||||
|
: isEditing
|
||||||
|
? t("common.save")
|
||||||
|
: t("common.add")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,8 @@
|
|||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "Config Wizard",
|
"useWizard": "Config Wizard",
|
||||||
"syncOtherSide": "Mirror to {{target}}",
|
"syncOtherSide": "Mirror to {{target}}",
|
||||||
"syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten."
|
"syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten.",
|
||||||
|
"willOverwriteWarning": "Will overwrite existing config in {{target}}"
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP Configuration Wizard",
|
"title": "MCP Configuration Wizard",
|
||||||
|
|||||||
@@ -300,7 +300,8 @@
|
|||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "配置向导",
|
"useWizard": "配置向导",
|
||||||
"syncOtherSide": "同步到 {{target}}",
|
"syncOtherSide": "同步到 {{target}}",
|
||||||
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖"
|
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖",
|
||||||
|
"willOverwriteWarning": "将覆盖 {{target}} 中的同名配置"
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP 配置向导",
|
"title": "MCP 配置向导",
|
||||||
|
|||||||
Reference in New Issue
Block a user