mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-03 03:35:04 +03:00
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:
@@ -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());
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user