refactor(frontend): read the panel's navigation from one route manifest (#6922)

The router, the sidebar, the page titles and the command palette each kept
their own list of the panel's pages, so a page added to one could go
missing from another without any test noticing. They all read
src/routes/panelRoutes.tsx now, and a contract test holds the manifest to
the router, the translation bundle and the palette keywords.

The unused useZodForm helper goes, and CONTRIBUTING, frontend/CLAUDE.md and
the frontend README describe the useForm + rhfZodValidate pattern the
panel's forms actually use.
This commit is contained in:
25f2002654
2026-10-25 00:18:31 +05:30
committed by GitHub
parent 746bdbe9f4
commit 14c9bdff89
14 changed files with 242 additions and 254 deletions
+7 -3
View File
@@ -1,17 +1,21 @@
import { screen } from '@testing-library/react';
import { FormProvider } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, InputNumber } from 'antd';
import { describe, expect, it } from 'vitest';
import { z } from 'zod';
import { FormField, useZodForm } from '@/components/form/rhf';
import { FormField } from '@/components/form/rhf';
import { renderWithProviders } from './test-utils';
const Schema = z.object({ totalGB: z.number() });
function Harness() {
const methods = useZodForm(Schema, { defaultValues: { totalGB: 0 } });
const methods = useForm<z.infer<typeof Schema>>({
resolver: zodResolver(Schema),
defaultValues: { totalGB: 0 },
});
return (
<FormProvider {...methods}>
<Form layout="vertical">
@@ -0,0 +1,57 @@
import { readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { PALETTE_KEYWORDS } from '@/components/command-palette/CommandPalette';
import { PANEL_ROUTES, PANEL_ROUTE_PATHS } from '@/routes/panelRoutes';
import { routes } from '@/routes';
// Four hand-kept nav lists let a page go missing from the sidebar or fall back to the "3X-UI"
// title; the manifest replaced them, and these tests hold it to the router and the bundle.
const translationDir = resolve(process.cwd(), '..', 'internal', 'web', 'translation');
function flattenKeys(obj: Record<string, unknown>, prefix = ''): string[] {
const keys: string[] = [];
for (const [k, v] of Object.entries(obj)) {
const key = `${prefix}${k}`;
if (v !== null && typeof v === 'object') {
keys.push(...flattenKeys(v as Record<string, unknown>, `${key}.`));
} else {
keys.push(key);
}
}
return keys;
}
describe('panel navigation', () => {
it('lists every route once', () => {
expect(new Set(PANEL_ROUTE_PATHS).size).toBe(PANEL_ROUTE_PATHS.length);
});
it('routes every manifest path', () => {
const childPaths = (routes[0].children ?? []).map((child) =>
child.index ? '/' : `/${child.path ?? ''}`,
);
expect(childPaths.sort()).toEqual([...PANEL_ROUTE_PATHS].sort());
});
it('translates every title key', () => {
const enKeys = new Set(
flattenKeys(
JSON.parse(readFileSync(join(translationDir, 'en-US.json'), 'utf8')) as Record<
string,
unknown
>,
),
);
for (const route of PANEL_ROUTES) {
expect(enKeys.has(route.titleKey)).toBe(true);
}
});
it('gives every route command-palette keywords', () => {
expect(Object.keys(PALETTE_KEYWORDS).sort()).toEqual([...PANEL_ROUTE_PATHS].sort());
});
});
+5 -3
View File
@@ -1,10 +1,11 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { FormProvider } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, Input, InputNumber, Switch } from 'antd';
import { z } from 'zod';
import { FormField, useZodForm } from '@/components/form/rhf';
import { FormField } from '@/components/form/rhf';
const GB = 1024 * 1024 * 1024;
@@ -18,7 +19,8 @@ const Schema = z.object({
type Values = z.infer<typeof Schema>;
function Harness({ onSubmit }: { onSubmit: (values: Values) => void }) {
const methods = useZodForm(Schema, {
const methods = useForm<Values>({
resolver: zodResolver(Schema),
defaultValues: { email: '', enabled: false, slug: '', bytes: 2 * GB },
});
return (