Files
new-api/web/default/src/features/system-settings/maintenance/header-navigation-section.tsx
T

300 lines
9.5 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useMemo } from 'react'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import {
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Switch } from '@/components/ui/switch'
import {
SettingsControlChildren,
SettingsForm,
SettingsSwitchContent,
SettingsControlGroup,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
import {
HEADER_NAV_DEFAULT,
type HeaderNavModulesConfig,
serializeHeaderNavModules,
} from './config'
const headerNavSchema = z.object({
home: z.boolean(),
console: z.boolean(),
pricingEnabled: z.boolean(),
pricingRequireAuth: z.boolean(),
rankingsEnabled: z.boolean(),
rankingsRequireAuth: z.boolean(),
docs: z.boolean(),
about: z.boolean(),
})
type HeaderNavFormValues = z.infer<typeof headerNavSchema>
type HeaderNavigationSectionProps = {
config: HeaderNavModulesConfig
initialSerialized: string
}
const toFormValues = (config: HeaderNavModulesConfig): HeaderNavFormValues => ({
home:
config.home === undefined ? HEADER_NAV_DEFAULT.home : Boolean(config.home),
console:
config.console === undefined
? HEADER_NAV_DEFAULT.console
: Boolean(config.console),
pricingEnabled:
config.pricing?.enabled === undefined
? HEADER_NAV_DEFAULT.pricing.enabled
: Boolean(config.pricing.enabled),
pricingRequireAuth:
config.pricing?.requireAuth === undefined
? HEADER_NAV_DEFAULT.pricing.requireAuth
: Boolean(config.pricing.requireAuth),
rankingsEnabled:
config.rankings?.enabled === undefined
? HEADER_NAV_DEFAULT.rankings.enabled
: Boolean(config.rankings.enabled),
rankingsRequireAuth:
config.rankings?.requireAuth === undefined
? HEADER_NAV_DEFAULT.rankings.requireAuth
: Boolean(config.rankings.requireAuth),
docs:
config.docs === undefined ? HEADER_NAV_DEFAULT.docs : Boolean(config.docs),
about:
config.about === undefined
? HEADER_NAV_DEFAULT.about
: Boolean(config.about),
})
export function HeaderNavigationSection({
config,
initialSerialized,
}: HeaderNavigationSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const formDefaults = useMemo(() => toFormValues(config), [config])
const form = useForm<HeaderNavFormValues>({
resolver: zodResolver(headerNavSchema),
defaultValues: formDefaults,
})
useEffect(() => {
form.reset(formDefaults)
}, [formDefaults, form])
const onSubmit = async (values: HeaderNavFormValues) => {
const payload: HeaderNavModulesConfig = {
...config,
home: values.home,
console: values.console,
docs: values.docs,
about: values.about,
pricing: {
...(config.pricing ?? HEADER_NAV_DEFAULT.pricing),
enabled: values.pricingEnabled,
requireAuth: values.pricingRequireAuth,
},
rankings: {
...(config.rankings ?? HEADER_NAV_DEFAULT.rankings),
enabled: values.rankingsEnabled,
requireAuth: values.rankingsRequireAuth,
},
}
const serialized = serializeHeaderNavModules(payload)
if (serialized === initialSerialized) {
return
}
await updateOption.mutateAsync({
key: 'HeaderNavModules',
value: serialized,
})
}
const resetToDefault = () => {
form.reset(toFormValues(HEADER_NAV_DEFAULT))
}
const simpleModules: Array<{
key: keyof HeaderNavFormValues
title: string
description: string
}> = [
{
key: 'home',
title: t('Home'),
description: t('Landing page with system overview.'),
},
{
key: 'console',
title: t('Console'),
description: t('User dashboard and quota controls.'),
},
{
key: 'docs',
title: t('Docs'),
description: t('Documentation or external knowledge base.'),
},
{
key: 'about',
title: t('About'),
description: t('Static page describing the platform.'),
},
]
const accessModules: Array<{
enabledKey: keyof HeaderNavFormValues
requireAuthKey: keyof HeaderNavFormValues
requireAuthDependsOn: 'pricingEnabled' | 'rankingsEnabled'
title: string
description: string
requireAuthTitle: string
requireAuthDescription: string
}> = [
{
enabledKey: 'pricingEnabled',
requireAuthKey: 'pricingRequireAuth',
requireAuthDependsOn: 'pricingEnabled',
title: t('Model Square'),
description: t('Public model catalog and pricing page.'),
requireAuthTitle: t('Require login to view models'),
requireAuthDescription: t(
'Visitors must authenticate before accessing the pricing directory.'
),
},
{
enabledKey: 'rankingsEnabled',
requireAuthKey: 'rankingsRequireAuth',
requireAuthDependsOn: 'rankingsEnabled',
title: t('Rankings'),
description: t('Public rankings page based on live usage data.'),
requireAuthTitle: t('Require login to view rankings'),
requireAuthDescription: t(
'Visitors must authenticate before accessing the rankings page.'
),
},
]
return (
<SettingsSection title={t('Header navigation')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
onReset={resetToDefault}
isSaving={updateOption.isPending}
resetLabel='Reset to default'
saveLabel='Save navigation'
/>
<div className='grid gap-4 md:grid-cols-2'>
{simpleModules.map((module) => (
<FormField
key={module.key}
control={form.control}
name={module.key}
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{module.title}</FormLabel>
<FormDescription>{module.description}</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</SettingsSwitchItem>
)}
/>
))}
</div>
<div className='grid gap-4 lg:grid-cols-2'>
{accessModules.map((module) => (
<SettingsControlGroup key={module.enabledKey}>
<FormField
control={form.control}
name={module.enabledKey}
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{module.title}</FormLabel>
<FormDescription>{module.description}</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name={module.requireAuthKey}
render={({ field }) => (
<SettingsControlChildren>
<SettingsSwitchItem className='py-2'>
<SettingsSwitchContent>
<FormLabel>{module.requireAuthTitle}</FormLabel>
<FormDescription>
{module.requireAuthDescription}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!form.watch(module.requireAuthDependsOn)}
/>
</FormControl>
<FormMessage />
</SettingsSwitchItem>
</SettingsControlChildren>
)}
/>
</SettingsControlGroup>
))}
</div>
</SettingsForm>
</Form>
</SettingsSection>
)
}