Skip to content

Commit 95e4661

Browse files
committed
fix: node docs url and settings context stability
1 parent bf37bcb commit 95e4661

File tree

2 files changed

+80
-66
lines changed

2 files changed

+80
-66
lines changed

dashboard/src/pages/_dashboard.settings.tsx

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
44
import { useGetSettings, useModifySettings } from '@/service/api'
55
import { useQueryClient } from '@tanstack/react-query'
66
import { Bell, Database, ListTodo, LucideIcon, MessageCircle, Palette, Send, Settings as SettingsIcon, Webhook } from 'lucide-react'
7-
import { createContext, useContext } from 'react'
7+
import { createContext, useContext, useMemo, useCallback } from 'react'
88
import { useTranslation } from 'react-i18next'
99
import { Outlet, useLocation, useNavigate } from 'react-router'
1010
import { toast } from 'sonner'
@@ -134,80 +134,89 @@ export default function Settings() {
134134
})
135135

136136
// Wrapper function to filter data based on active tab (only for sudo admins)
137-
const handleUpdateSettings = (data: any) => {
138-
if (!is_sudo) return // No-op for non-sudo admins
139-
140-
let filteredData: any = {}
141-
142-
// Only include data relevant to the active tab
143-
switch (activeTab) {
144-
case 'notifications':
145-
if (data.data) {
146-
// If data is already wrapped, use it as is
147-
filteredData = data
148-
} else {
149-
// Wrap notification data in the expected format
150-
filteredData = {
151-
data: {
152-
notification_enable: data.notification_enable,
153-
notification_settings: data.notification_settings,
154-
},
137+
const handleUpdateSettings = useCallback(
138+
(data: any) => {
139+
if (!is_sudo) return // No-op for non-sudo admins
140+
141+
let filteredData: any = {}
142+
143+
// Only include data relevant to the active tab
144+
switch (activeTab) {
145+
case 'notifications':
146+
if (data.data) {
147+
// If data is already wrapped, use it as is
148+
filteredData = data
149+
} else {
150+
// Wrap notification data in the expected format
151+
filteredData = {
152+
data: {
153+
notification_enable: data.notification_enable,
154+
notification_settings: data.notification_settings,
155+
},
156+
}
155157
}
156-
}
157-
break
158-
case 'subscriptions':
159-
if (data.subscription) {
160-
// Wrap subscription data in the expected format
161-
filteredData = {
162-
data: {
163-
subscription: data.subscription,
164-
},
158+
break
159+
case 'subscriptions':
160+
if (data.subscription) {
161+
// Wrap subscription data in the expected format
162+
filteredData = {
163+
data: {
164+
subscription: data.subscription,
165+
},
166+
}
167+
} else {
168+
// If data is already wrapped, use it as is
169+
filteredData = data
165170
}
166-
} else {
167-
// If data is already wrapped, use it as is
168-
filteredData = data
169-
}
170-
break
171-
case 'telegram':
172-
// Add telegram specific filtering if needed
173-
filteredData = { data: data }
174-
break
175-
case 'discord':
176-
// Add discord specific filtering if needed
177-
filteredData = { data: data }
178-
break
179-
case 'webhook':
180-
// Add webhook specific filtering if needed
181-
filteredData = { data: data }
182-
break
183-
case 'cleanup':
184-
// Add cleanup specific filtering if needed
185-
filteredData = { data: data }
186-
break
187-
case 'theme':
188-
// Theme settings are client-side only, no API call needed
189-
return
190-
default:
191-
filteredData = { data: data }
192-
}
193-
194-
updateSettings(filteredData)
195-
}
171+
break
172+
case 'telegram':
173+
// Add telegram specific filtering if needed
174+
filteredData = { data: data }
175+
break
176+
case 'discord':
177+
// Add discord specific filtering if needed
178+
filteredData = { data: data }
179+
break
180+
case 'webhook':
181+
// Add webhook specific filtering if needed
182+
filteredData = { data: data }
183+
break
184+
case 'cleanup':
185+
// Add cleanup specific filtering if needed
186+
filteredData = { data: data }
187+
break
188+
case 'theme':
189+
// Theme settings are client-side only, no API call needed
190+
return
191+
default:
192+
filteredData = { data: data }
193+
}
196194

197-
const settingsContextValue: SettingsContextType = {
198-
settings: is_sudo ? settings : {}, // Non-sudo admins don't need settings data
199-
isLoading: is_sudo ? isLoading : false,
200-
error: is_sudo ? error : null,
201-
updateSettings: is_sudo ? handleUpdateSettings : () => {}, // No-op for non-sudo admins
202-
isSaving: is_sudo ? isSaving : false,
203-
}
195+
updateSettings(filteredData)
196+
},
197+
[is_sudo, activeTab, updateSettings],
198+
)
199+
200+
// Memoize context value to ensure stability during HMR
201+
const settingsContextValue: SettingsContextType = useMemo(
202+
() => ({
203+
settings: is_sudo ? (settings || {}) : {}, // Non-sudo admins don't need settings data
204+
isLoading: is_sudo ? isLoading : false,
205+
error: is_sudo ? error : null,
206+
updateSettings: is_sudo ? handleUpdateSettings : () => {}, // No-op for non-sudo admins
207+
isSaving: is_sudo ? isSaving : false,
208+
}),
209+
[is_sudo, settings, isLoading, error, isSaving, handleUpdateSettings],
210+
)
204211

205212
// Generate tutorial URL for the current settings tab
206213
const getTutorialUrl = () => {
207214
const locale = i18n.language || 'en'
208215
return `https://docs.pasarguard.org/${locale}/panel/settings`
209216
}
210217

218+
// Always render the provider to ensure context is available for child routes
219+
// This prevents issues during HMR when components might render before parent is ready
211220
return (
212221
<SettingsContext.Provider value={settingsContextValue}>
213222
<div className="flex w-full flex-col items-start gap-0">

dashboard/src/utils/docs-url.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import i18n from '@/locales/i18n'
44
/**
55
* Generates a documentation URL for a given page path
66
* Format: docs.pasarguard.org/{locale}/panel/{page}
7+
* Special case: /nodes routes use docs.pasarguard.org/{locale}/node/
78
*
89
* @param pagePath - The page path (e.g., '/settings', '/users', '/nodes/cores')
910
* @returns The full documentation URL
@@ -13,13 +14,17 @@ export function getDocsUrl(pagePath: string): string {
1314
// Normalize locale (e.g., 'en-US' -> 'en')
1415
const normalizedLocale = locale.split('-')[0]
1516

17+
// Special case: node documentation uses /node/ instead of /panel/nodes
18+
if (pagePath.startsWith('/nodes')) {
19+
return `${DOCUMENTATION}/${normalizedLocale}/node/`
20+
}
21+
1622
// Map route paths to documentation paths
1723
const pathMap: Record<string, string> = {
1824
'/': 'dashboard',
1925
'/users': 'users',
2026
'/statistics': 'statistics',
2127
'/hosts': 'hosts',
22-
'/nodes': 'nodes',
2328
'/groups': 'groups',
2429
'/templates': 'templates',
2530
'/admins': 'admins',

0 commit comments

Comments
 (0)