refactor: extract validation logic into useMcpValidation hook
Extract all MCP form validation logic into a reusable custom hook to improve code organization and enable reuse across components. Changes: - Create useMcpValidation hook with 4 validation functions: * validateJson: basic JSON structure validation * formatTomlError: unified TOML error formatting with i18n * validateTomlConfig: complete TOML validation with required fields * validateJsonConfig: complete JSON validation with structure checks - Update McpFormModal to use the hook instead of inline validation - Simplify validation calls throughout the component - Reduce code duplication while maintaining all functionality Benefits: - Validation logic can be reused in other MCP-related components - Easier to test validation in isolation - Better separation of concerns - McpFormModal remains at 699 lines (original: 767), kept cohesive The component stays as one piece since its 700 lines represent a single, cohesive form feature rather than multiple unrelated concerns.
This commit is contained in:
@@ -29,11 +29,11 @@ import {
|
||||
translateMcpBackendError,
|
||||
} from "../../utils/errorUtils";
|
||||
import {
|
||||
validateToml,
|
||||
tomlToMcpServer,
|
||||
extractIdFromToml,
|
||||
mcpServerToToml,
|
||||
} from "../../utils/tomlUtils";
|
||||
import { useMcpValidation } from "./useMcpValidation";
|
||||
|
||||
interface McpFormModalProps {
|
||||
appType: AppType;
|
||||
@@ -68,29 +68,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatTomlError, validateTomlConfig, validateJsonConfig } =
|
||||
useMcpValidation();
|
||||
|
||||
// JSON 基本校验(返回 i18n 文案)
|
||||
const validateJson = (text: string): string => {
|
||||
if (!text.trim()) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
return "";
|
||||
} catch {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
};
|
||||
|
||||
// 统一格式化 TOML 错误(本地化 + 详情)
|
||||
const formatTomlError = (err: string): string => {
|
||||
if (!err) return "";
|
||||
if (err === "mustBeObject" || err === "parseError") {
|
||||
return t("mcp.error.tomlInvalid");
|
||||
}
|
||||
return `${t("mcp.error.tomlInvalid")}: ${err}`;
|
||||
};
|
||||
const [formId, setFormId] = useState(
|
||||
() => editingId || initialData?.id || "",
|
||||
);
|
||||
@@ -231,14 +211,11 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
if (useToml) {
|
||||
const toml = mcpServerToToml(presetWithDesc.server);
|
||||
setFormConfig(toml);
|
||||
{
|
||||
const err = validateToml(toml);
|
||||
setConfigError(formatTomlError(err));
|
||||
}
|
||||
setConfigError(validateTomlConfig(toml));
|
||||
} else {
|
||||
const json = JSON.stringify(presetWithDesc.server, null, 2);
|
||||
setFormConfig(json);
|
||||
setConfigError(validateJson(json));
|
||||
setConfigError(validateJsonConfig(json));
|
||||
}
|
||||
setSelectedPreset(index);
|
||||
};
|
||||
@@ -261,71 +238,27 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
setFormConfig(value);
|
||||
|
||||
if (useToml) {
|
||||
// TOML 校验
|
||||
const err = validateToml(value);
|
||||
// TOML validation (use hook's complete validation)
|
||||
const err = validateTomlConfig(value);
|
||||
if (err) {
|
||||
setConfigError(formatTomlError(err));
|
||||
setConfigError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试解析并做必填字段提示
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const server = tomlToMcpServer(value);
|
||||
if (server.type === "stdio" && !server.command?.trim()) {
|
||||
setConfigError(t("mcp.error.commandRequired"));
|
||||
return;
|
||||
}
|
||||
if (server.type === "http" && !server.url?.trim()) {
|
||||
setConfigError(t("mcp.wizard.urlRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试提取 ID(如果用户还没有填写)
|
||||
if (!formId.trim()) {
|
||||
// Try to extract ID (if user hasn't filled it yet)
|
||||
if (value.trim() && !formId.trim()) {
|
||||
const extractedId = extractIdFromToml(value);
|
||||
if (extractedId) {
|
||||
setFormId(extractedId);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || String(e);
|
||||
setConfigError(formatTomlError(msg));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 校验
|
||||
const baseErr = validateJson(value);
|
||||
if (baseErr) {
|
||||
setConfigError(baseErr);
|
||||
// JSON validation (use hook's complete validation)
|
||||
const err = validateJsonConfig(value);
|
||||
if (err) {
|
||||
setConfigError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 进一步结构校验
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const obj = JSON.parse(value);
|
||||
if (obj && typeof obj === "object") {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
|
||||
setConfigError(t("mcp.error.singleServerObjectRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
const typ = (obj as any)?.type;
|
||||
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
||||
setConfigError(t("mcp.error.commandRequired"));
|
||||
return;
|
||||
}
|
||||
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
||||
setConfigError(t("mcp.wizard.urlRequired"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析异常已在基础校验覆盖
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setConfigError("");
|
||||
@@ -336,20 +269,19 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
if (!formName.trim()) {
|
||||
setFormName(title);
|
||||
}
|
||||
// Wizard 返回的是 JSON,根据格式决定是否需要转换
|
||||
// Wizard returns JSON, convert based on format if needed
|
||||
if (useToml) {
|
||||
try {
|
||||
const server = JSON.parse(json) as McpServerSpec;
|
||||
const toml = mcpServerToToml(server);
|
||||
setFormConfig(toml);
|
||||
const err = validateToml(toml);
|
||||
setConfigError(formatTomlError(err));
|
||||
setConfigError(validateTomlConfig(toml));
|
||||
} catch (e: any) {
|
||||
setConfigError(t("mcp.error.jsonInvalid"));
|
||||
}
|
||||
} else {
|
||||
setFormConfig(json);
|
||||
setConfigError(validateJson(json));
|
||||
setConfigError(validateJsonConfig(json));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -366,20 +298,20 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证配置格式
|
||||
// Validate configuration format
|
||||
let serverSpec: McpServerSpec;
|
||||
|
||||
if (useToml) {
|
||||
// TOML 模式
|
||||
const tomlError = validateToml(formConfig);
|
||||
setConfigError(formatTomlError(tomlError));
|
||||
// TOML mode
|
||||
const tomlError = validateTomlConfig(formConfig);
|
||||
setConfigError(tomlError);
|
||||
if (tomlError) {
|
||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formConfig.trim()) {
|
||||
// 空配置
|
||||
// Empty configuration
|
||||
serverSpec = {
|
||||
type: "stdio",
|
||||
command: "",
|
||||
@@ -396,8 +328,8 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 模式
|
||||
const jsonError = validateJson(formConfig);
|
||||
// JSON mode
|
||||
const jsonError = validateJsonConfig(formConfig);
|
||||
setConfigError(jsonError);
|
||||
if (jsonError) {
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
||||
@@ -405,7 +337,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
}
|
||||
|
||||
if (!formConfig.trim()) {
|
||||
// 空配置
|
||||
// Empty configuration
|
||||
serverSpec = {
|
||||
type: "stdio",
|
||||
command: "",
|
||||
|
||||
94
src/components/mcp/useMcpValidation.ts
Normal file
94
src/components/mcp/useMcpValidation.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { validateToml, tomlToMcpServer } from "@/utils/tomlUtils";
|
||||
|
||||
export function useMcpValidation() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// JSON basic validation (returns i18n text)
|
||||
const validateJson = (text: string): string => {
|
||||
if (!text.trim()) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
return "";
|
||||
} catch {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
};
|
||||
|
||||
// Unified TOML error formatting (localization + details)
|
||||
const formatTomlError = (err: string): string => {
|
||||
if (!err) return "";
|
||||
if (err === "mustBeObject" || err === "parseError") {
|
||||
return t("mcp.error.tomlInvalid");
|
||||
}
|
||||
return `${t("mcp.error.tomlInvalid")}: ${err}`;
|
||||
};
|
||||
|
||||
// Full TOML validation (including required field checks)
|
||||
const validateTomlConfig = (value: string): string => {
|
||||
const err = validateToml(value);
|
||||
if (err) {
|
||||
return formatTomlError(err);
|
||||
}
|
||||
|
||||
// Try to parse and check required fields
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const server = tomlToMcpServer(value);
|
||||
if (server.type === "stdio" && !server.command?.trim()) {
|
||||
return t("mcp.error.commandRequired");
|
||||
}
|
||||
if (server.type === "http" && !server.url?.trim()) {
|
||||
return t("mcp.wizard.urlRequired");
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || String(e);
|
||||
return formatTomlError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// Full JSON validation (including structure checks)
|
||||
const validateJsonConfig = (value: string): string => {
|
||||
const baseErr = validateJson(value);
|
||||
if (baseErr) {
|
||||
return baseErr;
|
||||
}
|
||||
|
||||
// Further structure validation
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const obj = JSON.parse(value);
|
||||
if (obj && typeof obj === "object") {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
|
||||
return t("mcp.error.singleServerObjectRequired");
|
||||
}
|
||||
|
||||
const typ = (obj as any)?.type;
|
||||
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
||||
return t("mcp.error.commandRequired");
|
||||
}
|
||||
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
||||
return t("mcp.wizard.urlRequired");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Parse errors already covered by base validation
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
return {
|
||||
validateJson,
|
||||
formatTomlError,
|
||||
validateTomlConfig,
|
||||
validateJsonConfig,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user