From ea5401cf0a882665958cc9079ebc11998aaf1305 Mon Sep 17 00:00:00 2001 From: wenjie Date: Fri, 6 Mar 2026 20:00:07 +0800 Subject: [PATCH] fix(web): harden channel config updates and resolve frontend lint issues - validate channel PUT/PATCH updates before saving and return structured validation errors - require `enabled` in toggle requests to avoid silent false defaults - support editing `allow_origins` in the generic channel form and parse string/array inputs on backend - replace channel form `any` usage with `ChannelConfig` (`Record`) and add safe value helpers - add i18n strings for allow-origins fields and apply related frontend formatting cleanups --- web/backend/api/channels.go | 82 ++++++++++++-- web/backend/dist/.gitkeep | 0 web/frontend/src/api/channels.ts | 6 +- web/frontend/src/components/app-layout.tsx | 2 +- web/frontend/src/components/app-sidebar.tsx | 4 +- .../src/components/channels/channel-card.tsx | 6 +- .../channels/channel-forms/discord-form.tsx | 26 +++-- .../channels/channel-forms/feishu-form.tsx | 34 ++++-- .../channels/channel-forms/generic-form.tsx | 69 ++++++++++-- .../channels/channel-forms/slack-form.tsx | 30 +++-- .../channels/channel-forms/telegram-form.tsx | 28 +++-- .../src/components/channels/channels-page.tsx | 4 +- .../channels/edit-channel-sheet.tsx | 20 ++-- web/frontend/src/i18n/locales/en.json | 3 + web/frontend/src/i18n/locales/zh.json | 3 + web/frontend/src/main.tsx | 7 +- web/frontend/src/routes/config.tsx | 104 ++++++++++++------ 17 files changed, 302 insertions(+), 126 deletions(-) delete mode 100644 web/backend/dist/.gitkeep diff --git a/web/backend/api/channels.go b/web/backend/api/channels.go index 550c1b900..93e59b3ff 100644 --- a/web/backend/api/channels.go +++ b/web/backend/api/channels.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/sipeed/picoclaw/pkg/config" ) @@ -105,6 +106,10 @@ func (h *Handler) handleUpdateChannel(w http.ResponseWriter, r *http.Request) { } applyChannelUpdate(name, &cfg.Channels, incoming) + if errs := validateConfig(cfg); len(errs) > 0 { + writeValidationErrors(w, errs) + return + } if err := config.SaveConfig(h.configPath, cfg); err != nil { http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) @@ -133,12 +138,16 @@ func (h *Handler) handleToggleChannel(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var req struct { - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` } if err = json.Unmarshal(body, &req); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } + if req.Enabled == nil { + http.Error(w, "Missing required field: enabled", http.StatusBadRequest) + return + } cfg, err := config.LoadConfig(h.configPath) if err != nil { @@ -146,7 +155,11 @@ func (h *Handler) handleToggleChannel(w http.ResponseWriter, r *http.Request) { return } - setChannelEnabled(name, &cfg.Channels, req.Enabled) + setChannelEnabled(name, &cfg.Channels, *req.Enabled) + if errs := validateConfig(cfg); len(errs) > 0 { + writeValidationErrors(w, errs) + return + } if err := config.SaveConfig(h.configPath, cfg); err != nil { http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) @@ -166,6 +179,15 @@ func isValidChannel(name string) bool { return false } +func writeValidationErrors(w http.ResponseWriter, errs []string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]any{ + "status": "validation_error", + "errors": errs, + }) +} + // extractChannelInfo returns enabled, configured status and masked config for a channel. func extractChannelInfo(name string, ch *config.ChannelsConfig) (bool, bool, map[string]any) { var enabled, configured bool @@ -407,6 +429,50 @@ func applyChannelUpdate(name string, ch *config.ChannelsConfig, incoming map[str } return nil } + getStringArray := func(key string) ([]string, bool) { + v, ok := incoming[key] + if !ok { + return nil, false + } + + switch arr := v.(type) { + case []any: + result := make([]string, 0, len(arr)) + for _, item := range arr { + if s, ok := item.(string); ok { + s = strings.TrimSpace(s) + if s != "" { + result = append(result, s) + } + } + } + return result, true + case []string: + result := make([]string, 0, len(arr)) + for _, s := range arr { + s = strings.TrimSpace(s) + if s != "" { + result = append(result, s) + } + } + return result, true + case string: + if strings.TrimSpace(arr) == "" { + return []string{}, true + } + parts := strings.Split(arr, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + return result, true + default: + return nil, false + } + } getGroupTrigger := func() config.GroupTriggerConfig { if v, ok := incoming["group_trigger"]; ok { if m, ok := v.(map[string]any); ok { @@ -583,16 +649,8 @@ func applyChannelUpdate(name string, ch *config.ChannelsConfig, incoming map[str c.Enabled = getBool("enabled") c.Token = preserveSecret(getString("token"), c.Token) c.AllowTokenQuery = getBool("allow_token_query") - if v, ok := incoming["allow_origins"]; ok { - if arr, ok := v.([]any); ok { - origins := make([]string, 0, len(arr)) - for _, item := range arr { - if s, ok := item.(string); ok { - origins = append(origins, s) - } - } - c.AllowOrigins = origins - } + if origins, ok := getStringArray("allow_origins"); ok { + c.AllowOrigins = origins } c.PingInterval = getInt("ping_interval") c.ReadTimeout = getInt("read_timeout") diff --git a/web/backend/dist/.gitkeep b/web/backend/dist/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/frontend/src/api/channels.ts b/web/frontend/src/api/channels.ts index 3610b7ed4..f4b7d343e 100644 --- a/web/frontend/src/api/channels.ts +++ b/web/frontend/src/api/channels.ts @@ -1,11 +1,13 @@ // API client for channel management. +export type ChannelConfig = Record + export interface ChannelInfo { name: string display_name: string enabled: boolean configured: boolean - config: Record + config: ChannelConfig } interface ChannelsListResponse { @@ -32,7 +34,7 @@ export async function getChannels(): Promise { export async function updateChannel( name: string, - config: Record, + config: ChannelConfig, ): Promise { return request(`/api/channels/${name}`, { method: "PUT", diff --git a/web/frontend/src/components/app-layout.tsx b/web/frontend/src/components/app-layout.tsx index cfa7e019f..ff9877bae 100644 --- a/web/frontend/src/components/app-layout.tsx +++ b/web/frontend/src/components/app-layout.tsx @@ -24,4 +24,4 @@ export function AppLayout({ children }: { children: ReactNode }) { ) -} \ No newline at end of file +} diff --git a/web/frontend/src/components/app-sidebar.tsx b/web/frontend/src/components/app-sidebar.tsx index 8d302e020..b541755f8 100644 --- a/web/frontend/src/components/app-sidebar.tsx +++ b/web/frontend/src/components/app-sidebar.tsx @@ -46,9 +46,7 @@ const navGroups = [ { label: "navigation.channels_group", defaultOpen: true, - items: [ - { title: "navigation.channels", url: "/channels", icon: IconPlug }, - ], + items: [{ title: "navigation.channels", url: "/channels", icon: IconPlug }], }, { label: "navigation.services", diff --git a/web/frontend/src/components/channels/channel-card.tsx b/web/frontend/src/components/channels/channel-card.tsx index fa844ede4..5cece1a33 100644 --- a/web/frontend/src/components/channels/channel-card.tsx +++ b/web/frontend/src/components/channels/channel-card.tsx @@ -29,7 +29,7 @@ export function ChannelCard({ {channel.display_name} {channel.configured ? ( - + {t("channels.status.configured")} ) : ( @@ -54,9 +54,7 @@ export function ChannelCard({ onClick={() => onEdit(channel)} > - - {t("channels.action.configure")} - + {t("channels.action.configure")} diff --git a/web/frontend/src/components/channels/channel-forms/discord-form.tsx b/web/frontend/src/components/channels/channel-forms/discord-form.tsx index 7e0a98018..f602f2a82 100644 --- a/web/frontend/src/components/channels/channel-forms/discord-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/discord-form.tsx @@ -1,18 +1,28 @@ import { useTranslation } from "react-i18next" -import { Input } from "@/components/ui/input" +import type { ChannelConfig } from "@/api/channels" import { AdvancedSection, Field, KeyInput, } from "@/components/models/shared-form" +import { Input } from "@/components/ui/input" interface DiscordFormProps { - config: Record - onChange: (key: string, value: any) => void + config: ChannelConfig + onChange: (key: string, value: unknown) => void isEdit: boolean } +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + export function DiscordForm({ config, onChange, isEdit }: DiscordFormProps) { const { t } = useTranslation() @@ -21,16 +31,16 @@ export function DiscordForm({ config, onChange, isEdit }: DiscordFormProps) { onChange("_token", v)} placeholder={ - isEdit && config.token + isEdit && asString(config.token) ? t("channels.field.secretPlaceholderSet") : t("channels.field.tokenPlaceholder") } @@ -43,7 +53,7 @@ export function DiscordForm({ config, onChange, isEdit }: DiscordFormProps) { hint={t("channels.field.proxyHint")} > onChange("proxy", e.target.value)} placeholder="http://127.0.0.1:7890" /> @@ -53,7 +63,7 @@ export function DiscordForm({ config, onChange, isEdit }: DiscordFormProps) { hint={t("channels.field.allowFromHint")} > onChange( "allow_from", diff --git a/web/frontend/src/components/channels/channel-forms/feishu-form.tsx b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx index c7befe29d..f6cd1ebe2 100644 --- a/web/frontend/src/components/channels/channel-forms/feishu-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx @@ -1,18 +1,28 @@ import { useTranslation } from "react-i18next" -import { Input } from "@/components/ui/input" +import type { ChannelConfig } from "@/api/channels" import { AdvancedSection, Field, KeyInput, } from "@/components/models/shared-form" +import { Input } from "@/components/ui/input" interface FeishuFormProps { - config: Record - onChange: (key: string, value: any) => void + config: ChannelConfig + onChange: (key: string, value: unknown) => void isEdit: boolean } +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) { const { t } = useTranslation() @@ -20,7 +30,7 @@ export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) {
onChange("app_id", e.target.value)} placeholder="cli_xxxx" /> @@ -29,16 +39,16 @@ export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) { onChange("_app_secret", v)} placeholder={ - isEdit && config.app_secret + isEdit && asString(config.app_secret) ? t("channels.field.secretPlaceholderSet") : t("channels.field.secretPlaceholder") } @@ -48,10 +58,10 @@ export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) { onChange("_verification_token", v)} placeholder={ - isEdit && config.verification_token + isEdit && asString(config.verification_token) ? t("channels.field.secretPlaceholderSet") : t("channels.field.secretPlaceholder") } @@ -59,10 +69,10 @@ export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) { onChange("_encrypt_key", v)} placeholder={ - isEdit && config.encrypt_key + isEdit && asString(config.encrypt_key) ? t("channels.field.secretPlaceholderSet") : t("channels.field.secretPlaceholder") } @@ -73,7 +83,7 @@ export function FeishuForm({ config, onChange, isEdit }: FeishuFormProps) { hint={t("channels.field.allowFromHint")} > onChange( "allow_from", diff --git a/web/frontend/src/components/channels/channel-forms/generic-form.tsx b/web/frontend/src/components/channels/channel-forms/generic-form.tsx index 441b2d1e9..5ee753f4b 100644 --- a/web/frontend/src/components/channels/channel-forms/generic-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/generic-form.tsx @@ -1,12 +1,14 @@ import { useTranslation } from "react-i18next" -import { Input } from "@/components/ui/input" +import type { ChannelConfig } from "@/api/channels" import { Field, KeyInput } from "@/components/models/shared-form" +import { Input } from "@/components/ui/input" +import { Switch } from "@/components/ui/switch" interface GenericFormProps { channelName: string - config: Record - onChange: (key: string, value: any) => void + config: ChannelConfig + onChange: (key: string, value: unknown) => void isEdit: boolean } @@ -35,6 +37,7 @@ const OBJECT_FIELDS = new Set([ "typing", "placeholder", "allow_from", + "allow_origins", ]) function formatLabel(key: string): string { @@ -44,11 +47,16 @@ function formatLabel(key: string): string { .join(" ") } -export function GenericForm({ - config, - onChange, - isEdit, -}: GenericFormProps) { +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +export function GenericForm({ config, onChange, isEdit }: GenericFormProps) { const { t } = useTranslation() const fields = Object.keys(config).filter( @@ -71,10 +79,10 @@ export function GenericForm({ } > onChange(editKey, v)} placeholder={ - isEdit && config[key] + isEdit && Boolean(config[key]) ? t("channels.field.secretPlaceholderSet") : "" } @@ -85,7 +93,17 @@ export function GenericForm({ const value = config[key] if (typeof value === "boolean") { - return null // Booleans are less common in generic; skip for now + return ( + +
+ onChange(key, checked)} + aria-label={formatLabel(key)} + /> +
+
+ ) } return ( @@ -113,7 +131,7 @@ export function GenericForm({ hint={t("channels.field.allowFromHint")} > onChange( "allow_from", @@ -127,6 +145,33 @@ export function GenericForm({ />
)} + + {config.allow_origins !== undefined && ( + + + onChange( + "allow_origins", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t( + "channels.field.allowOriginsPlaceholder", + "e.g. https://example.com, http://localhost:5173", + )} + /> + + )}
) } diff --git a/web/frontend/src/components/channels/channel-forms/slack-form.tsx b/web/frontend/src/components/channels/channel-forms/slack-form.tsx index 44f8c8a44..2114c4d4a 100644 --- a/web/frontend/src/components/channels/channel-forms/slack-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/slack-form.tsx @@ -1,18 +1,28 @@ import { useTranslation } from "react-i18next" -import { Input } from "@/components/ui/input" +import type { ChannelConfig } from "@/api/channels" import { AdvancedSection, Field, KeyInput, } from "@/components/models/shared-form" +import { Input } from "@/components/ui/input" interface SlackFormProps { - config: Record - onChange: (key: string, value: any) => void + config: ChannelConfig + onChange: (key: string, value: unknown) => void isEdit: boolean } +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + export function SlackForm({ config, onChange, isEdit }: SlackFormProps) { const { t } = useTranslation() @@ -21,16 +31,16 @@ export function SlackForm({ config, onChange, isEdit }: SlackFormProps) { onChange("_bot_token", v)} placeholder={ - isEdit && config.bot_token + isEdit && asString(config.bot_token) ? t("channels.field.secretPlaceholderSet") : "xoxb-xxxx" } @@ -40,16 +50,16 @@ export function SlackForm({ config, onChange, isEdit }: SlackFormProps) { onChange("_app_token", v)} placeholder={ - isEdit && config.app_token + isEdit && asString(config.app_token) ? t("channels.field.secretPlaceholderSet") : "xapp-xxxx" } @@ -62,7 +72,7 @@ export function SlackForm({ config, onChange, isEdit }: SlackFormProps) { hint={t("channels.field.allowFromHint")} > onChange( "allow_from", diff --git a/web/frontend/src/components/channels/channel-forms/telegram-form.tsx b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx index 0c2c0f8bd..d73b84a89 100644 --- a/web/frontend/src/components/channels/channel-forms/telegram-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx @@ -1,18 +1,28 @@ import { useTranslation } from "react-i18next" -import { Input } from "@/components/ui/input" +import type { ChannelConfig } from "@/api/channels" import { AdvancedSection, Field, KeyInput, } from "@/components/models/shared-form" +import { Input } from "@/components/ui/input" interface TelegramFormProps { - config: Record - onChange: (key: string, value: any) => void + config: ChannelConfig + onChange: (key: string, value: unknown) => void isEdit: boolean } +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + export function TelegramForm({ config, onChange, isEdit }: TelegramFormProps) { const { t } = useTranslation() @@ -21,16 +31,16 @@ export function TelegramForm({ config, onChange, isEdit }: TelegramFormProps) { onChange("_token", v)} placeholder={ - isEdit && config.token + isEdit && asString(config.token) ? t("channels.field.secretPlaceholderSet") : t("channels.field.tokenPlaceholder") } @@ -40,7 +50,7 @@ export function TelegramForm({ config, onChange, isEdit }: TelegramFormProps) { onChange("base_url", e.target.value)} placeholder="https://api.telegram.org" /> @@ -50,7 +60,7 @@ export function TelegramForm({ config, onChange, isEdit }: TelegramFormProps) { hint={t("channels.field.proxyHint")} > onChange("proxy", e.target.value)} placeholder="http://127.0.0.1:7890" /> @@ -60,7 +70,7 @@ export function TelegramForm({ config, onChange, isEdit }: TelegramFormProps) { hint={t("channels.field.allowFromHint")} > onChange( "allow_from", diff --git a/web/frontend/src/components/channels/channels-page.tsx b/web/frontend/src/components/channels/channels-page.tsx index 225711375..28dc6e944 100644 --- a/web/frontend/src/components/channels/channels-page.tsx +++ b/web/frontend/src/components/channels/channels-page.tsx @@ -30,9 +30,7 @@ export function ChannelsPage() { setChannels(sorted) setFetchError("") } catch (e) { - setFetchError( - e instanceof Error ? e.message : t("channels.loadError"), - ) + setFetchError(e instanceof Error ? e.message : t("channels.loadError")) } finally { setLoading(false) } diff --git a/web/frontend/src/components/channels/edit-channel-sheet.tsx b/web/frontend/src/components/channels/edit-channel-sheet.tsx index 4337ba99d..7a6ea222c 100644 --- a/web/frontend/src/components/channels/edit-channel-sheet.tsx +++ b/web/frontend/src/components/channels/edit-channel-sheet.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react" import { useTranslation } from "react-i18next" -import type { ChannelInfo } from "@/api/channels" +import type { ChannelConfig, ChannelInfo } from "@/api/channels" import { updateChannel } from "@/api/channels" import { DiscordForm } from "@/components/channels/channel-forms/discord-form" import { FeishuForm } from "@/components/channels/channel-forms/feishu-form" @@ -42,8 +42,8 @@ const SECRET_FIELD_MAP: Record = { verification_token: "_verification_token", } -function buildEditConfig(config: Record): Record { - const edit: Record = { ...config } +function buildEditConfig(config: ChannelConfig): ChannelConfig { + const edit: ChannelConfig = { ...config } // Initialize edit buffer keys for secrets as empty (user fills new values) for (const secretKey of Object.keys(SECRET_FIELD_MAP)) { if (secretKey in config) { @@ -55,9 +55,9 @@ function buildEditConfig(config: Record): Record { function buildSavePayload( channel: ChannelInfo, - editConfig: Record, -): Record { - const payload: Record = { enabled: channel.enabled } + editConfig: ChannelConfig, +): ChannelConfig { + const payload: ChannelConfig = { enabled: channel.enabled } for (const [key, value] of Object.entries(editConfig)) { // Skip the edit-buffer underscore keys — we use them to populate real keys @@ -81,7 +81,7 @@ export function EditChannelSheet({ onSaved, }: EditChannelSheetProps) { const { t } = useTranslation() - const [editConfig, setEditConfig] = useState>({}) + const [editConfig, setEditConfig] = useState({}) const [saving, setSaving] = useState(false) const [serverError, setServerError] = useState("") @@ -92,7 +92,7 @@ export function EditChannelSheet({ } }, [channel]) - const handleChange = useCallback((key: string, value: any) => { + const handleChange = useCallback((key: string, value: unknown) => { setEditConfig((prev) => ({ ...prev, [key]: value })) }, []) @@ -171,9 +171,7 @@ export function EditChannelSheet({ name: channel?.display_name ?? "", })} - - {t("channels.edit.description")} - + {t("channels.edit.description")}
{renderForm()}
diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index 1b71454d8..65684d072 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -247,6 +247,9 @@ "allowFrom": "Allow From", "allowFromHint": "Comma-separated list of allowed user/group IDs. Leave empty to allow all.", "allowFromPlaceholder": "e.g. 123456, 789012", + "allowOrigins": "Allow Origins", + "allowOriginsHint": "Comma-separated list of allowed origins. Leave empty to allow all.", + "allowOriginsPlaceholder": "e.g. https://example.com, http://localhost:5173", "secretPlaceholder": "Enter secret", "secretPlaceholderSet": "Leave blank to keep existing", "secretHintSet": "A value is already set. Leave blank to keep it unchanged." diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index 4a1da3f4b..c4f9e6315 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -247,6 +247,9 @@ "allowFrom": "允许来源", "allowFromHint": "用逗号分隔的用户/群组 ID 列表,留空表示允许所有。", "allowFromPlaceholder": "例如 123456, 789012", + "allowOrigins": "允许来源域名", + "allowOriginsHint": "用逗号分隔允许的 Origin,留空表示允许所有。", + "allowOriginsPlaceholder": "例如 https://example.com, http://localhost:5173", "secretPlaceholder": "输入密钥", "secretPlaceholderSet": "留空保持原有值不变", "secretHintSet": "已设置密钥,留空表示不修改。" diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx index eae0cb89a..81e72c29f 100644 --- a/web/frontend/src/main.tsx +++ b/web/frontend/src/main.tsx @@ -1,7 +1,4 @@ -import { - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { RouterProvider, createRouter } from "@tanstack/react-router" import { StrictMode } from "react" import ReactDOM from "react-dom/client" @@ -35,4 +32,4 @@ if (!rootElement.innerHTML) { , ) -} \ No newline at end of file +} diff --git a/web/frontend/src/routes/config.tsx b/web/frontend/src/routes/config.tsx index 0a7255596..aa507abee 100644 --- a/web/frontend/src/routes/config.tsx +++ b/web/frontend/src/routes/config.tsx @@ -1,18 +1,10 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { createFileRoute } from "@tanstack/react-router" +import { useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { PageHeader } from "@/components/page-header" -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Textarea } from "@/components/ui/textarea" -import { ScrollArea } from "@/components/ui/scroll-area" import { AlertDialog, AlertDialogAction, @@ -24,8 +16,16 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { useState } from "react" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Textarea } from "@/components/ui/textarea" export const Route = createFileRoute("/config")({ component: ConfigPage, @@ -72,7 +72,9 @@ function RawJsonPanel() { } }, 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) @@ -94,7 +96,10 @@ function RawJsonPanel() { const [isDirty, setIsDirty] = useState(false) // Store the last saved config to detect changes - const [lastSavedConfig, setLastSavedConfig] = useState | null>(null) + const [lastSavedConfig, setLastSavedConfig] = useState | null>(null) // Initialize editor value when config is first loaded const getInitialEditorValue = () => { @@ -103,7 +108,7 @@ function RawJsonPanel() { } return editorValue } - + const displayValue = getInitialEditorValue() const handleSave = () => { @@ -112,7 +117,12 @@ function RawJsonPanel() { JSON.parse(editorValue) mutation.mutate(editorValue) } catch (error) { - toast.error(t("pages.config.invalid_json", error instanceof Error ? error.message : "Invalid JSON format.")) + toast.error( + t( + "pages.config.invalid_json", + error instanceof Error ? error.message : "Invalid JSON format.", + ), + ) } } @@ -120,9 +130,16 @@ function RawJsonPanel() { try { const formatted = JSON.stringify(JSON.parse(editorValue), null, 2) setEditorValue(formatted) - toast.success(t("pages.config.format_success", "JSON formatted successfully.")) + toast.success( + t("pages.config.format_success", "JSON formatted successfully."), + ) } catch (error) { - toast.error(t("pages.config.format_error", error instanceof Error ? error.message : "Invalid JSON format.")) + toast.error( + t( + "pages.config.format_error", + error instanceof Error ? error.message : "Invalid JSON format.", + ), + ) } } @@ -137,7 +154,12 @@ function RawJsonPanel() { setEditorValue(JSON.stringify(config, null, 2)) } setIsDirty(false) - toast.info(t("pages.config.reset_success", "Changes have been reset to the last saved state.")) + toast.info( + t( + "pages.config.reset_success", + "Changes have been reset to the last saved state.", + ), + ) setShowResetDialog(false) } @@ -161,12 +183,12 @@ function RawJsonPanel() { ) : (
- {isDirty && ( -
- {t("pages.config.unsaved_changes", "You have unsaved changes.")} -
- )} -
+ {isDirty && ( +
+ {t("pages.config.unsaved_changes", "You have unsaved changes.")} +
+ )} +