fix(web): useEffect resets editorValue whenever config changes

This commit is contained in:
Dihubopen 2026-03-06 15:35:43 +08:00
parent 686f5d449e
commit ac08300d9f
3 changed files with 65 additions and 22 deletions

View file

@ -73,7 +73,8 @@
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"saving": "Saving...", "saving": "Saving...",
"reset": "Reset" "reset": "Reset",
"confirm": "Confirm"
}, },
"models": { "models": {
"description": "Configure API keys for each AI provider. Only configured models are available for chat.", "description": "Configure API keys for each AI provider. Only configured models are available for chat.",
@ -163,11 +164,14 @@
"json_placeholder": "Enter valid JSON configuration...", "json_placeholder": "Enter valid JSON configuration...",
"save_success": "Configuration saved successfully.", "save_success": "Configuration saved successfully.",
"save_error": "Failed to save configuration.", "save_error": "Failed to save configuration.",
"reset_success": "Configuration has been reset.", "reset_confirm_title": "Reset Changes",
"reset_confirm_desc": "Are you sure you want to reset your unsaved changes back to the last saved state?",
"reset_success": "Changes have been reset to the last saved state.",
"invalid_json": "Invalid JSON format.", "invalid_json": "Invalid JSON format.",
"format_success": "JSON formatted successfully.", "format_success": "JSON formatted successfully.",
"format_error": "Invalid JSON format.", "format_error": "Invalid JSON format.",
"format": "Format" "format": "Format",
"lose_unsaved_changes": "You have unsaved changes. Are you sure you want to reset and lose these changes?"
}, },
"logs": { "logs": {
"description": "System logs and monitoring." "description": "System logs and monitoring."

View file

@ -73,7 +73,8 @@
"cancel": "取消", "cancel": "取消",
"save": "保存", "save": "保存",
"saving": "保存中...", "saving": "保存中...",
"reset": "重置" "reset": "重置",
"confirm": "确认"
}, },
"models": { "models": {
"description": "为每个 AI 服务商配置 API Key。只有已配置的模型可用于对话。", "description": "为每个 AI 服务商配置 API Key。只有已配置的模型可用于对话。",
@ -163,11 +164,15 @@
"json_placeholder": "请输入有效的 JSON 配置...", "json_placeholder": "请输入有效的 JSON 配置...",
"save_success": "配置保存成功。", "save_success": "配置保存成功。",
"save_error": "配置保存失败。", "save_error": "配置保存失败。",
"reset_success": "配置已重置。", "reset_confirm_title": "重置更改",
"reset_confirm_desc": "您确定要重置回上次保存的状态吗?",
"reset_success": "更改已重置为上次保存的状态。",
"invalid_json": "JSON 格式无效。", "invalid_json": "JSON 格式无效。",
"format_success": "JSON 格式化成功。", "format_success": "JSON 格式化成功。",
"format_error": "JSON 格式无效。", "format_error": "JSON 格式无效。",
"format": "格式化" "format": "格式化",
"lose_unsaved_changes": "您有未保存的更改。确定要重置并丢失这些更改吗?",
"unsaved_changes": "您有未保存的更改。"
}, },
"logs": { "logs": {
"description": "系统日志和监控。" "description": "系统日志和监控。"

View file

@ -36,7 +36,7 @@ function ConfigPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader title={t("navigation.config", "Config")} /> <PageHeader title={t("navigation.config", "Config")} />
<div className="flex-1 overflow-auto p-4 lg:p-8"> <div className="flex-1 overflow-auto p-3 lg:p-6">
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<RawJsonPanel /> <RawJsonPanel />
</div> </div>
@ -73,7 +73,17 @@ function RawJsonPanel() {
}, },
onSuccess: () => { onSuccess: () => {
toast.success(t("pages.config.save_success", "Configuration saved successfully.")) toast.success(t("pages.config.save_success", "Configuration saved successfully."))
// Update last saved config and reset dirty state
try {
const savedConfig = JSON.parse(editorValue)
setLastSavedConfig(savedConfig)
setIsDirty(false)
// Important: Invalidate the query to refresh the cached data
queryClient.invalidateQueries({ queryKey: ["config"] }) queryClient.invalidateQueries({ queryKey: ["config"] })
} catch (error) {
// If JSON parsing fails, invalidate to get fresh data
queryClient.invalidateQueries({ queryKey: ["config"] })
}
}, },
onError: () => { onError: () => {
toast.error(t("pages.config.save_error", "Failed to save configuration.")) toast.error(t("pages.config.save_error", "Failed to save configuration."))
@ -81,20 +91,29 @@ function RawJsonPanel() {
}) })
const [editorValue, setEditorValue] = useState("") const [editorValue, setEditorValue] = useState("")
const [isDirty, setIsDirty] = useState(false)
// Store the last saved config to detect changes
const [lastSavedConfig, setLastSavedConfig] = useState<any>(null)
useEffect(() => { useEffect(() => {
if (config) { if (config && JSON.stringify(config) !== JSON.stringify(lastSavedConfig)) {
// Only update if there are no unsaved changes or if this is the initial load
if (!isDirty || !lastSavedConfig) {
setEditorValue(JSON.stringify(config, null, 2)) setEditorValue(JSON.stringify(config, null, 2))
setLastSavedConfig(config)
setIsDirty(false)
} }
}, [config]) }
}, [config, lastSavedConfig, isDirty])
const handleSave = () => { const handleSave = () => {
try { try {
// Validate JSON before saving // Validate JSON before saving
JSON.parse(editorValue) JSON.parse(editorValue)
mutation.mutate(editorValue) mutation.mutate(editorValue)
} catch (e) { } catch (error) {
toast.error(t("pages.config.invalid_json", "Invalid JSON format.")) toast.error(t("pages.config.invalid_json", error instanceof Error ? error.message : "Invalid JSON format."))
} }
} }
@ -104,15 +123,22 @@ function RawJsonPanel() {
setEditorValue(formatted) setEditorValue(formatted)
toast.success(t("pages.config.format_success", "JSON formatted successfully.")) toast.success(t("pages.config.format_success", "JSON formatted successfully."))
} catch (error) { } catch (error) {
toast.error(t("pages.config.format_error", "Invalid JSON format.")) toast.error(t("pages.config.format_error", error instanceof Error ? error.message : "Invalid JSON format."))
} }
} }
const [showResetDialog, setShowResetDialog] = useState(false) const [showResetDialog, setShowResetDialog] = useState(false)
const confirmReset = () => { const confirmReset = () => {
queryClient.invalidateQueries({ queryKey: ["config"] }) // Reset editor content to the last saved configuration
toast.info(t("pages.config.reset_success", "Configuration has been reset.")) if (lastSavedConfig) {
setEditorValue(JSON.stringify(lastSavedConfig, null, 2))
} else if (config) {
// Fallback to current config if no last saved config
setEditorValue(JSON.stringify(config, null, 2))
}
setIsDirty(false)
toast.info(t("pages.config.reset_success", "Changes have been reset to the last saved state."))
setShowResetDialog(false) setShowResetDialog(false)
} }
@ -135,12 +161,20 @@ function RawJsonPanel() {
<p>{t("labels.loading", "Loading...")}</p> <p>{t("labels.loading", "Loading...")}</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-3">
{isDirty && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-2 text-sm text-yellow-700">
{t("pages.config.unsaved_changes", "You have unsaved changes.")}
</div>
)}
<div className="bg-muted/30 relative rounded-lg border"> <div className="bg-muted/30 relative rounded-lg border">
<ScrollArea className="h-[calc(100vh-20rem)] min-h-[200px]"> <ScrollArea className="h-[calc(100vh-20rem)] min-h-[200px]">
<Textarea <Textarea
value={editorValue} value={editorValue}
onChange={(e) => setEditorValue(e.target.value)} onChange={(e) => {
setEditorValue(e.target.value)
setIsDirty(true)
}}
className="font-mono text-sm min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 shadow-none focus-visible:ring-0" className="font-mono text-sm min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 shadow-none focus-visible:ring-0"
placeholder={t( placeholder={t(
"pages.config.json_placeholder", "pages.config.json_placeholder",
@ -157,7 +191,7 @@ function RawJsonPanel() {
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
variant="outline" variant="outline"
disabled={mutation.isPending} disabled={!isDirty}
onClick={() => setShowResetDialog(true)} onClick={() => setShowResetDialog(true)}
> >
{t("common.reset", "Reset")} {t("common.reset", "Reset")}
@ -165,9 +199,9 @@ function RawJsonPanel() {
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t("pages.config.reset_confirm_title", "Reset Configuration")}</AlertDialogTitle> <AlertDialogTitle>{t("pages.config.reset_confirm_title", "Reset Changes")}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t("pages.config.reset_confirm_desc", "Are you sure you want to reset the configuration? This action cannot be undone.")} {t("pages.config.reset_confirm_desc", "Are you sure you want to reset your unsaved changes back to the last saved state?")}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>