refactor(mcp): improve form modal layout with adaptive height editor
Restructure MCP form modal for better space utilization. - Split form into upper form fields and lower JSON editor sections - Add full-height mode support for JsonEditor component - Use flex layout for editor to fill available space - Update PromptFormPanel to use full-height editor - Fix locale text formatting
This commit is contained in:
@@ -247,14 +247,23 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFullHeight = height === "100%";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div
|
||||||
<div ref={editorRef} style={{ width: "100%" }} />
|
style={{ width: "100%", height: isFullHeight ? "100%" : "auto" }}
|
||||||
|
className={isFullHeight ? "flex flex-col" : ""}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
style={{ width: "100%", height: isFullHeight ? undefined : "auto" }}
|
||||||
|
className={isFullHeight ? "flex-1 min-h-0" : ""}
|
||||||
|
/>
|
||||||
{language === "json" && (
|
{language === "json" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleFormat}
|
onClick={handleFormat}
|
||||||
className="mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
className={`${isFullHeight ? "mt-2 flex-shrink-0" : "mt-2"} inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors`}
|
||||||
>
|
>
|
||||||
<Wand2 className="w-3.5 h-3.5" />
|
<Wand2 className="w-3.5 h-3.5" />
|
||||||
{t("common.format", { defaultValue: "格式化" })}
|
{t("common.format", { defaultValue: "格式化" })}
|
||||||
|
|||||||
@@ -429,209 +429,210 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="glass rounded-xl p-6 border border-white/10 space-y-6">
|
<div className="flex flex-col h-full gap-6">
|
||||||
{/* 预设选择(仅新增时展示) */}
|
{/* 上半部分:表单字段 */}
|
||||||
{!isEditing && (
|
<div className="glass rounded-xl p-6 border border-white/10 space-y-6 flex-shrink-0">
|
||||||
|
{/* 预设选择(仅新增时展示) */}
|
||||||
|
{!isEditing && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-3">
|
||||||
|
{t("mcp.presets.title")}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={applyCustom}
|
||||||
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${selectedPreset === -1
|
||||||
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
||||||
|
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t("presetSelector.custom")}
|
||||||
|
</button>
|
||||||
|
{mcpPresets.map((preset, idx) => {
|
||||||
|
const descriptionKey = `mcp.presets.${preset.id}.description`;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={preset.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => applyPreset(idx)}
|
||||||
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${selectedPreset === idx
|
||||||
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
||||||
|
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
||||||
|
}`}
|
||||||
|
title={t(descriptionKey)}
|
||||||
|
>
|
||||||
|
{preset.id}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ID (标题) */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="block text-sm font-medium text-foreground">
|
||||||
|
{t("mcp.form.title")} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
{!isEditing && idError && (
|
||||||
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
|
{idError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.titlePlaceholder")}
|
||||||
|
value={formId}
|
||||||
|
onChange={(e) => handleIdChange(e.target.value)}
|
||||||
|
disabled={isEditing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
{t("mcp.form.name")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.namePlaceholder")}
|
||||||
|
value={formName}
|
||||||
|
onChange={(e) => setFormName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 启用到哪些应用 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-3">
|
<label className="block text-sm font-medium text-foreground mb-3">
|
||||||
{t("mcp.presets.title")}
|
{t("mcp.form.enabledApps")}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-4">
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
type="button"
|
<Checkbox
|
||||||
onClick={applyCustom}
|
id="enable-claude"
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
checked={enabledApps.claude}
|
||||||
selectedPreset === -1
|
onCheckedChange={(checked: boolean) =>
|
||||||
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
setEnabledApps({ ...enabledApps, claude: checked })
|
||||||
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
}
|
||||||
}`}
|
/>
|
||||||
>
|
<label
|
||||||
{t("presetSelector.custom")}
|
htmlFor="enable-claude"
|
||||||
</button>
|
className="text-sm text-foreground cursor-pointer select-none"
|
||||||
{mcpPresets.map((preset, idx) => {
|
>
|
||||||
const descriptionKey = `mcp.presets.${preset.id}.description`;
|
{t("mcp.unifiedPanel.apps.claude")}
|
||||||
return (
|
</label>
|
||||||
<button
|
</div>
|
||||||
key={preset.id}
|
|
||||||
type="button"
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => applyPreset(idx)}
|
<Checkbox
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
id="enable-codex"
|
||||||
selectedPreset === idx
|
checked={enabledApps.codex}
|
||||||
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
onCheckedChange={(checked: boolean) =>
|
||||||
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
setEnabledApps({ ...enabledApps, codex: checked })
|
||||||
}`}
|
}
|
||||||
title={t(descriptionKey)}
|
/>
|
||||||
>
|
<label
|
||||||
{preset.id}
|
htmlFor="enable-codex"
|
||||||
</button>
|
className="text-sm text-foreground 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-foreground cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
{t("mcp.unifiedPanel.apps.gemini")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ID (标题) */}
|
{/* 可折叠的附加信息按钮 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<button
|
||||||
<label className="block text-sm font-medium text-foreground">
|
type="button"
|
||||||
{t("mcp.form.title")} <span className="text-red-500">*</span>
|
onClick={() => setShowMetadata(!showMetadata)}
|
||||||
</label>
|
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
{!isEditing && idError && (
|
>
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
{showMetadata ? (
|
||||||
{idError}
|
<ChevronUp size={16} />
|
||||||
</span>
|
) : (
|
||||||
)}
|
<ChevronDown size={16} />
|
||||||
|
)}
|
||||||
|
{t("mcp.form.additionalInfo")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
|
||||||
type="text"
|
{/* 附加信息区域(可折叠) */}
|
||||||
placeholder={t("mcp.form.titlePlaceholder")}
|
{showMetadata && (
|
||||||
value={formId}
|
<>
|
||||||
onChange={(e) => handleIdChange(e.target.value)}
|
<div>
|
||||||
disabled={isEditing}
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
/>
|
{t("mcp.form.description")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.descriptionPlaceholder")}
|
||||||
|
value={formDescription}
|
||||||
|
onChange={(e) => setFormDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
{t("mcp.form.tags")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.tagsPlaceholder")}
|
||||||
|
value={formTags}
|
||||||
|
onChange={(e) => setFormTags(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
{t("mcp.form.homepage")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.homepagePlaceholder")}
|
||||||
|
value={formHomepage}
|
||||||
|
onChange={(e) => setFormHomepage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
{t("mcp.form.docs")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("mcp.form.docsPlaceholder")}
|
||||||
|
value={formDocs}
|
||||||
|
onChange={(e) => setFormDocs(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name */}
|
{/* 下半部分:JSON 配置编辑器 - 自适应剩余高度 */}
|
||||||
<div>
|
<div className="glass rounded-xl p-6 border border-white/10 flex flex-col flex-1 min-h-0">
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<div className="flex items-center justify-between mb-4 flex-shrink-0">
|
||||||
{t("mcp.form.name")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("mcp.form.namePlaceholder")}
|
|
||||||
value={formName}
|
|
||||||
onChange={(e) => setFormName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 启用到哪些应用 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-foreground 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-foreground 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-foreground 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-foreground cursor-pointer select-none"
|
|
||||||
>
|
|
||||||
{t("mcp.unifiedPanel.apps.gemini")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 可折叠的附加信息按钮 */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowMetadata(!showMetadata)}
|
|
||||||
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
{showMetadata ? (
|
|
||||||
<ChevronUp size={16} />
|
|
||||||
) : (
|
|
||||||
<ChevronDown size={16} />
|
|
||||||
)}
|
|
||||||
{t("mcp.form.additionalInfo")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 附加信息区域(可折叠) */}
|
|
||||||
{showMetadata && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
|
||||||
{t("mcp.form.description")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("mcp.form.descriptionPlaceholder")}
|
|
||||||
value={formDescription}
|
|
||||||
onChange={(e) => setFormDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
|
||||||
{t("mcp.form.tags")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("mcp.form.tagsPlaceholder")}
|
|
||||||
value={formTags}
|
|
||||||
onChange={(e) => setFormTags(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
|
||||||
{t("mcp.form.homepage")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("mcp.form.homepagePlaceholder")}
|
|
||||||
value={formHomepage}
|
|
||||||
onChange={(e) => setFormHomepage(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
|
||||||
{t("mcp.form.docs")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("mcp.form.docsPlaceholder")}
|
|
||||||
value={formDocs}
|
|
||||||
onChange={(e) => setFormDocs(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 配置输入框 */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<label className="text-sm font-medium text-foreground">
|
<label className="text-sm font-medium text-foreground">
|
||||||
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
|
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
|
||||||
</label>
|
</label>
|
||||||
@@ -645,26 +646,30 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<JsonEditor
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
value={formConfig}
|
<div className="flex-1 min-h-0">
|
||||||
onChange={handleConfigChange}
|
<JsonEditor
|
||||||
placeholder={
|
value={formConfig}
|
||||||
useToml
|
onChange={handleConfigChange}
|
||||||
? t("mcp.form.tomlPlaceholder")
|
placeholder={
|
||||||
: t("mcp.form.jsonPlaceholder")
|
useToml
|
||||||
}
|
? t("mcp.form.tomlPlaceholder")
|
||||||
darkMode={isDarkMode}
|
: t("mcp.form.jsonPlaceholder")
|
||||||
rows={12}
|
}
|
||||||
showValidation={!useToml}
|
darkMode={isDarkMode}
|
||||||
language={useToml ? "javascript" : "json"}
|
rows={12}
|
||||||
height="300px"
|
showValidation={!useToml}
|
||||||
/>
|
language={useToml ? "javascript" : "json"}
|
||||||
{configError && (
|
height="100%"
|
||||||
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
|
/>
|
||||||
<AlertCircle size={16} />
|
|
||||||
<span>{configError}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{configError && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm flex-shrink-0">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
<span>{configError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FullScreenPanel>
|
</FullScreenPanel>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
|
|||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
placeholder={t("prompts.contentPlaceholder", { filename })}
|
placeholder={t("prompts.contentPlaceholder", { filename })}
|
||||||
darkMode={isDarkMode}
|
darkMode={isDarkMode}
|
||||||
minHeight="500px"
|
minHeight="167px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user