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 { linter, Diagnostic } from "@codemirror/lint";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wand2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { formatJSON } from "@/utils/formatters";
|
||||
|
||||
interface JsonEditorProps {
|
||||
value: string;
|
||||
@@ -170,7 +173,44 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
}
|
||||
}, [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;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wand2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { formatJSON } from "@/utils/formatters";
|
||||
|
||||
interface CodexAuthSectionProps {
|
||||
value: string;
|
||||
@@ -19,6 +22,25 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
||||
}) => {
|
||||
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 (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
@@ -47,13 +69,26 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!error && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.authJsonHint")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -141,9 +176,11 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
||||
)}
|
||||
|
||||
{!configError && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.configTomlHint")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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 {
|
||||
value: string;
|
||||
@@ -37,6 +39,44 @@ export function CommonConfigEditor({
|
||||
}: CommonConfigEditorProps) {
|
||||
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 (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -97,12 +137,22 @@ export function CommonConfigEditor({
|
||||
data-gramm_editor="false"
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFormatMain}
|
||||
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>
|
||||
|
||||
<Dialog
|
||||
open={isModalOpen}
|
||||
@@ -137,12 +187,22 @@ export function CommonConfigEditor({
|
||||
data-gramm_editor="false"
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onModalClose}>
|
||||
{t("common.cancel")}
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
"unknown": "Unknown",
|
||||
"enterValidValue": "Please enter a valid value",
|
||||
"clear": "Clear",
|
||||
"toggleTheme": "Toggle theme"
|
||||
"toggleTheme": "Toggle theme",
|
||||
"format": "Format",
|
||||
"formatSuccess": "Formatted successfully",
|
||||
"formatError": "Format failed: {{error}}"
|
||||
},
|
||||
"apiKeyInput": {
|
||||
"placeholder": "Enter API Key",
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
"unknown": "未知",
|
||||
"enterValidValue": "请输入有效的内容",
|
||||
"clear": "清除",
|
||||
"toggleTheme": "切换主题"
|
||||
"toggleTheme": "切换主题",
|
||||
"format": "格式化",
|
||||
"formatSuccess": "格式化成功",
|
||||
"formatError": "格式化失败:{{error}}"
|
||||
},
|
||||
"apiKeyInput": {
|
||||
"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