feat(editor): add JSON format button to editors
Add one-click format functionality for JSON editors with toast notifications: - Add format button to JsonEditor (CodeMirror) - Add format button to CodexAuthSection (auth.json) - Add format button to CommonConfigEditor (settings + modal) - Add formatJSON utility function - Add i18n keys: format, formatSuccess, formatError Note: TOML formatting was intentionally NOT added to avoid losing comments during parse/stringify operations. TOML validation remains available via the existing useCodexTomlValidation hook.
This commit is contained in:
@@ -7,6 +7,9 @@ import { EditorState } from "@codemirror/state";
|
|||||||
import { placeholder } from "@codemirror/view";
|
import { placeholder } from "@codemirror/view";
|
||||||
import { linter, Diagnostic } from "@codemirror/lint";
|
import { linter, Diagnostic } from "@codemirror/lint";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Wand2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { formatJSON } from "@/utils/formatters";
|
||||||
|
|
||||||
interface JsonEditorProps {
|
interface JsonEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -170,7 +173,44 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
|||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return <div ref={editorRef} style={{ width: "100%" }} />;
|
// 格式化处理函数
|
||||||
|
const handleFormat = () => {
|
||||||
|
if (!viewRef.current) return;
|
||||||
|
|
||||||
|
const currentValue = viewRef.current.state.doc.toString();
|
||||||
|
if (!currentValue.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formatted = formatJSON(currentValue);
|
||||||
|
onChange(formatted);
|
||||||
|
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(
|
||||||
|
t("common.formatError", {
|
||||||
|
defaultValue: "格式化失败:{{error}}",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div ref={editorRef} style={{ width: "100%" }} />
|
||||||
|
{language === "json" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Wand2 className="w-3.5 h-3.5" />
|
||||||
|
{t("common.format", { defaultValue: "格式化" })}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JsonEditor;
|
export default JsonEditor;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Wand2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { formatJSON } from "@/utils/formatters";
|
||||||
|
|
||||||
interface CodexAuthSectionProps {
|
interface CodexAuthSectionProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -19,6 +22,25 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleFormat = () => {
|
||||||
|
if (!value.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formatted = formatJSON(value);
|
||||||
|
onChange(formatted);
|
||||||
|
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(
|
||||||
|
t("common.formatError", {
|
||||||
|
defaultValue: "格式化失败:{{error}}",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label
|
<label
|
||||||
@@ -47,13 +69,26 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
|||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
|
<button
|
||||||
)}
|
type="button"
|
||||||
|
onClick={handleFormat}
|
||||||
|
className="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" />
|
||||||
|
{t("common.format", { defaultValue: "格式化" })}
|
||||||
|
</button>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
{error && (
|
||||||
{t("codexConfig.authJsonHint")}
|
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
|
||||||
</p>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!error && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{t("codexConfig.authJsonHint")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -141,9 +176,11 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
|||||||
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
{!configError && (
|
||||||
{t("codexConfig.configTomlHint")}
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
</p>
|
{t("codexConfig.configTomlHint")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Save } from "lucide-react";
|
import { Save, Wand2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { formatJSON } from "@/utils/formatters";
|
||||||
|
|
||||||
interface CommonConfigEditorProps {
|
interface CommonConfigEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -37,6 +39,44 @@ export function CommonConfigEditor({
|
|||||||
}: CommonConfigEditorProps) {
|
}: CommonConfigEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleFormatMain = () => {
|
||||||
|
if (!value.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formatted = formatJSON(value);
|
||||||
|
onChange(formatted);
|
||||||
|
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(
|
||||||
|
t("common.formatError", {
|
||||||
|
defaultValue: "格式化失败:{{error}}",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormatModal = () => {
|
||||||
|
if (!commonConfigSnippet.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formatted = formatJSON(commonConfigSnippet);
|
||||||
|
onCommonConfigSnippetChange(formatted);
|
||||||
|
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(
|
||||||
|
t("common.formatError", {
|
||||||
|
defaultValue: "格式化失败:{{error}}",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -97,11 +137,21 @@ export function CommonConfigEditor({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("claudeConfig.fullSettingsHint", {
|
<button
|
||||||
defaultValue: "请填写完整的 Claude Code 配置",
|
type="button"
|
||||||
})}
|
onClick={handleFormatMain}
|
||||||
</p>
|
className="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" />
|
||||||
|
{t("common.format", { defaultValue: "格式化" })}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("claudeConfig.fullSettingsHint", {
|
||||||
|
defaultValue: "请填写完整的 Claude Code 配置",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -137,11 +187,21 @@ export function CommonConfigEditor({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
{commonConfigError && (
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">
|
<button
|
||||||
{commonConfigError}
|
type="button"
|
||||||
</p>
|
onClick={handleFormatModal}
|
||||||
)}
|
className="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" />
|
||||||
|
{t("common.format", { defaultValue: "格式化" })}
|
||||||
|
</button>
|
||||||
|
{commonConfigError && (
|
||||||
|
<p className="text-sm text-red-500 dark:text-red-400">
|
||||||
|
{commonConfigError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={onModalClose}>
|
<Button type="button" variant="outline" onClick={onModalClose}>
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"enterValidValue": "Please enter a valid value",
|
"enterValidValue": "Please enter a valid value",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"toggleTheme": "Toggle theme"
|
"toggleTheme": "Toggle theme",
|
||||||
|
"format": "Format",
|
||||||
|
"formatSuccess": "Formatted successfully",
|
||||||
|
"formatError": "Format failed: {{error}}"
|
||||||
},
|
},
|
||||||
"apiKeyInput": {
|
"apiKeyInput": {
|
||||||
"placeholder": "Enter API Key",
|
"placeholder": "Enter API Key",
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"enterValidValue": "请输入有效的内容",
|
"enterValidValue": "请输入有效的内容",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"toggleTheme": "切换主题"
|
"toggleTheme": "切换主题",
|
||||||
|
"format": "格式化",
|
||||||
|
"formatSuccess": "格式化成功",
|
||||||
|
"formatError": "格式化失败:{{error}}"
|
||||||
},
|
},
|
||||||
"apiKeyInput": {
|
"apiKeyInput": {
|
||||||
"placeholder": "请输入API Key",
|
"placeholder": "请输入API Key",
|
||||||
|
|||||||
28
src/utils/formatters.ts
Normal file
28
src/utils/formatters.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 格式化 JSON 字符串
|
||||||
|
* @param value - 原始 JSON 字符串
|
||||||
|
* @returns 格式化后的 JSON 字符串(2 空格缩进)
|
||||||
|
* @throws 如果 JSON 格式无效
|
||||||
|
*/
|
||||||
|
export function formatJSON(value: string): string {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
return JSON.stringify(parsed, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOML 格式化功能已禁用
|
||||||
|
*
|
||||||
|
* 原因:smol-toml 的 parse/stringify 会丢失所有注释和原有排版。
|
||||||
|
* 由于 TOML 常用于配置文件,注释是重要的文档说明,丢失注释会造成严重的用户体验问题。
|
||||||
|
*
|
||||||
|
* 未来可选方案:
|
||||||
|
* - 使用 @ltd/j-toml(支持注释保留,但需额外依赖和复杂的 API)
|
||||||
|
* - 实现仅格式化缩进/空白的轻量级方案
|
||||||
|
* - 使用 toml-eslint-parser + 自定义生成器
|
||||||
|
*
|
||||||
|
* 暂时建议:依赖现有的 TOML 语法校验(useCodexTomlValidation),不提供格式化功能。
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user