Skip to content

Commit db17e03

Browse files
committed
fix(settings): update general settings handling and improve loading/error states
1 parent 8e67586 commit db17e03

6 files changed

Lines changed: 54 additions & 48 deletions

File tree

dashboard/public/statics/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@
142142
"cancelSuccess": "Changes cancelled and original settings restored",
143143
"defaultFlow": {
144144
"title": "Default VLESS Flow",
145-
"description": "Autofill the flow field for new VLESS users"
145+
"description": "Autofill the flow field for new VLESS users",
146+
"none": "None"
146147
},
147148
"defaultMethod": {
148149
"title": "Default Shadowsocks Method",

dashboard/public/statics/locales/fa.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,8 @@
419419
"cancelSuccess": "تغییرات لغو شد و تنظیمات اصلی بازیابی شد",
420420
"defaultFlow": {
421421
"title": "جریان VLESS پیش‌فرض",
422-
"description": "پر کردن خودکار فیلد جریان برای کاربران جدید VLESS"
422+
"description": "پر کردن خودکار فیلد جریان برای کاربران جدید VLESS",
423+
"none": "بدون"
423424
},
424425
"defaultMethod": {
425426
"title": "روش پیش‌فرض Shadowsocks",

dashboard/public/statics/locales/ru.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,8 @@
551551
"cancelSuccess": "Изменения отменены и исходные настройки восстановлены",
552552
"defaultFlow": {
553553
"title": "Поток VLESS по умолчанию",
554-
"description": "Автоматически заполнять поле потока для новых пользователей VLESS"
554+
"description": "Автоматически заполнять поле потока для новых пользователей VLESS",
555+
"none": "Нет"
555556
},
556557
"defaultMethod": {
557558
"title": "Метод Shadowsocks по умолчанию",

dashboard/public/statics/locales/zh.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@
563563
"cancelSuccess": "更改已取消,原始设置已恢复",
564564
"defaultFlow": {
565565
"title": "默认 VLESS 流",
566-
"description": "为新 VLESS 用户自动填写流字段"
566+
"description": "为新 VLESS 用户自动填写流字段",
567+
"none": ""
567568
},
568569
"defaultMethod": {
569570
"title": "默认 Shadowsocks 加密方式",

dashboard/src/pages/_dashboard.settings.general.tsx

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
66
import { Separator } from '@/components/ui/separator'
77
import { Skeleton } from '@/components/ui/skeleton'
88
import { DEFAULT_SHADOWSOCKS_METHOD } from '@/constants/Proxies'
9-
import { ShadowsocksMethods, XTLSFlows, useReconnectAllNode } from '@/service/api'
9+
import { ShadowsocksMethods, XTLSFlows, useGetGeneralSettings, useReconnectAllNode } from '@/service/api'
1010
import { queryClient } from '@/utils/query-client'
1111
import { zodResolver } from '@hookform/resolvers/zod'
12-
import { Loader2, RefreshCcw, XIcon } from 'lucide-react'
12+
import { Loader2, RefreshCcw } from 'lucide-react'
1313
import { useEffect, useState } from 'react'
1414
import { useForm } from 'react-hook-form'
1515
import { useTranslation } from 'react-i18next'
1616
import { toast } from 'sonner'
1717
import { z } from 'zod'
1818
import { useSettingsContext } from './_dashboard.settings'
1919

20+
/** Radix Select forbids `SelectItem value=""`; map API empty flow to this UI value. */
21+
const DEFAULT_FLOW_SELECT_NONE = '__pg_default_flow_none__'
22+
2023
// general settings validation schema
2124
const generalSettingsSchema = z.object({
2225
default_flow: z.string().default(''),
@@ -27,7 +30,12 @@ type GeneralSettingsFormInput = z.input<typeof generalSettingsSchema>
2730

2831
export default function General() {
2932
const { t } = useTranslation()
30-
const { settings, isLoading, error, updateSettings, isSaving } = useSettingsContext()
33+
const { isLoading, error, updateSettings, isSaving } = useSettingsContext()
34+
const {
35+
data: generalSettings,
36+
isLoading: isGeneralLoading,
37+
error: generalError,
38+
} = useGetGeneralSettings()
3139
const [isReconnectAllDialogOpen, setIsReconnectAllDialogOpen] = useState(false)
3240
const reconnectAllNodeMutation = useReconnectAllNode()
3341

@@ -40,11 +48,12 @@ export default function General() {
4048
})
4149

4250
useEffect(() => {
51+
if (!generalSettings) return
4352
form.reset({
44-
default_flow: settings?.general?.default_flow || '',
45-
default_method: settings?.general?.default_method || DEFAULT_SHADOWSOCKS_METHOD,
53+
default_flow: generalSettings.default_flow || '',
54+
default_method: generalSettings.default_method || DEFAULT_SHADOWSOCKS_METHOD,
4655
})
47-
}, [settings])
56+
}, [generalSettings, form])
4857

4958
const onSubmit = async (data: GeneralSettingsFormInput) => {
5059
try {
@@ -64,13 +73,12 @@ export default function General() {
6473
}
6574

6675
const handleCancel = () => {
67-
if (settings?.general) {
68-
form.reset({
69-
default_flow: '',
70-
default_method: DEFAULT_SHADOWSOCKS_METHOD,
71-
})
72-
toast.success(t('settings.general.cancelSuccess'))
73-
}
76+
if (!generalSettings) return
77+
form.reset({
78+
default_flow: generalSettings.default_flow ?? '',
79+
default_method: generalSettings.default_method || DEFAULT_SHADOWSOCKS_METHOD,
80+
})
81+
toast.success(t('settings.general.cancelSuccess'))
7482
}
7583

7684
const handleReconnectAll = async () => {
@@ -103,8 +111,10 @@ export default function General() {
103111
}
104112
}
105113

114+
const loadError = error || generalError
115+
106116
// TODO: skeleton needs to be improved
107-
if (isLoading) {
117+
if (isLoading || isGeneralLoading) {
108118
return (
109119
<div className="w-full p-4 sm:py-6 lg:py-8">
110120
<div className="space-y-6 sm:space-y-8 lg:space-y-10">
@@ -140,7 +150,7 @@ export default function General() {
140150
)
141151
}
142152

143-
if (error) {
153+
if (loadError) {
144154
return (
145155
<div className="flex min-h-[400px] items-center justify-center p-4 sm:py-6 lg:py-8">
146156
<div className="space-y-3 text-center">
@@ -151,14 +161,6 @@ export default function General() {
151161
)
152162
}
153163

154-
const clearField = (field: keyof GeneralSettingsFormInput) => {
155-
return (e: React.MouseEvent<HTMLButtonElement>) => {
156-
e.preventDefault()
157-
e.stopPropagation()
158-
form.setValue(field, '')
159-
}
160-
}
161-
162164
return (
163165
<div className="flex min-h-[calc(100vh-200px)] w-full flex-col">
164166
<Form {...form}>
@@ -170,28 +172,27 @@ export default function General() {
170172
control={form.control}
171173
name="default_flow"
172174
render={({ field }) => (
173-
<FormItem className="relative space-y-2">
175+
<FormItem className="space-y-2">
174176
<FormLabel className="flex items-center gap-2 text-xs font-medium sm:text-sm">{t('settings.general.defaultFlow.title')}</FormLabel>
175177
<FormControl>
176-
<Select value={field.value} onValueChange={field.onChange}>
178+
<Select
179+
value={field.value ? field.value : DEFAULT_FLOW_SELECT_NONE}
180+
onValueChange={v => field.onChange(v === DEFAULT_FLOW_SELECT_NONE ? '' : v)}
181+
>
177182
<SelectTrigger className="text-xs sm:text-sm">
178183
<SelectValue />
179184
</SelectTrigger>
180-
{field.value && (
181-
<Button size="icon" variant="ghost" className="absolute right-8 top-6" onClick={clearField('default_flow')}>
182-
<XIcon />
183-
</Button>
184-
)}
185185
<SelectContent>
186+
<SelectItem value={DEFAULT_FLOW_SELECT_NONE} className="text-xs sm:text-sm">
187+
{t('settings.general.defaultFlow.none')}
188+
</SelectItem>
186189
{Object.values(XTLSFlows)
187-
.filter(Boolean)
188-
.map(flow => {
189-
return (
190-
<SelectItem value={flow} key={flow} className="text-xs sm:text-sm">
191-
{flow}
192-
</SelectItem>
193-
)
194-
})}
190+
.filter((flow): flow is Exclude<typeof flow, ''> => flow !== '')
191+
.map(flow => (
192+
<SelectItem value={flow} key={flow} className="text-xs sm:text-sm">
193+
{flow}
194+
</SelectItem>
195+
))}
195196
</SelectContent>
196197
</Select>
197198
</FormControl>

dashboard/src/pages/_dashboard.settings.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface SettingsContextType {
2121
settings: any
2222
isLoading: boolean
2323
error: any
24-
updateSettings: (data: any) => void
24+
updateSettings: (data: any) => Promise<void>
2525
isSaving: boolean
2626
}
2727

@@ -74,12 +74,13 @@ export default function Settings() {
7474
enabled: is_sudo, // Only fetch for sudo admins
7575
},
7676
})
77-
const { mutate: updateSettings, isPending: isSaving } = useModifySettings({
77+
const { mutateAsync: modifySettingsAsync, isPending: isSaving } = useModifySettings({
7878
mutation: {
7979
onSuccess: () => {
8080
toast.success(t(`settings.${activeTab}.saveSuccess`))
8181
// Invalidate settings query to refresh with new data from API response
8282
queryClient.invalidateQueries({ queryKey: ['/api/settings'] })
83+
queryClient.invalidateQueries({ queryKey: ['/api/settings/general'] })
8384
},
8485
onError: (error: any) => {
8586
// Extract validation errors from FetchError
@@ -135,7 +136,7 @@ export default function Settings() {
135136

136137
// Wrapper function to filter data based on active tab (only for sudo admins)
137138
const handleUpdateSettings = useCallback(
138-
(data: any) => {
139+
async (data: any) => {
139140
if (!is_sudo) return // No-op for non-sudo admins
140141

141142
let filteredData: any = {}
@@ -192,9 +193,9 @@ export default function Settings() {
192193
filteredData = { data: data }
193194
}
194195

195-
updateSettings(filteredData)
196+
await modifySettingsAsync(filteredData)
196197
},
197-
[is_sudo, activeTab, updateSettings],
198+
[is_sudo, activeTab, modifySettingsAsync],
198199
)
199200

200201
// Memoize context value to ensure stability during HMR
@@ -203,7 +204,7 @@ export default function Settings() {
203204
settings: is_sudo ? settings || {} : {}, // Non-sudo admins don't need settings data
204205
isLoading: is_sudo ? isLoading : false,
205206
error: is_sudo ? error : null,
206-
updateSettings: is_sudo ? handleUpdateSettings : () => { }, // No-op for non-sudo admins
207+
updateSettings: is_sudo ? handleUpdateSettings : async () => {}, // No-op for non-sudo admins
207208
isSaving: is_sudo ? isSaving : false,
208209
}),
209210
[is_sudo, settings, isLoading, error, isSaving, handleUpdateSettings],

0 commit comments

Comments
 (0)