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
@@ -2,11 +2,11 @@ import { useEffect } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { Button, Form, Input, InputNumber, Select, Switch, Typography } from 'antd';
import { FormProvider } from 'react-hook-form';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { FormField } from './FormField';
import { useZodForm } from './useZodForm';
const GB = 1024 * 1024 * 1024;
@@ -60,7 +60,8 @@ const ClientSchema = z.object({
});
function ClientDemo() {
const methods = useZodForm(ClientSchema, {
const methods = useForm<z.infer<typeof ClientSchema>>({
resolver: zodResolver(ClientSchema),
defaultValues: { email: 'user1@example.com', flow: 'xtls-rprx-vision', enable: true },
});
return (
@@ -95,8 +96,11 @@ const TrafficSchema = z.object({
});
function TrafficDemo() {
const methods = useZodForm(TrafficSchema, { defaultValues: { totalBytes: 50 * GB } });
const totalBytes = methods.watch('totalBytes');
const methods = useForm<z.infer<typeof TrafficSchema>>({
resolver: zodResolver(TrafficSchema),
defaultValues: { totalBytes: 50 * GB },
});
const totalBytes = useWatch({ control: methods.control, name: 'totalBytes' });
return (
<FormProvider {...methods}>
<Form layout="vertical" style={{ maxWidth: 360 }}>
@@ -128,7 +132,10 @@ const InboundSchema = z.object({
});
function ValidationDemo() {
const methods = useZodForm(InboundSchema, { defaultValues: { remark: '', port: 0 } });
const methods = useForm<z.infer<typeof InboundSchema>>({
resolver: zodResolver(InboundSchema),
defaultValues: { remark: '', port: 0 },
});
useEffect(() => {
void methods.trigger();
}, [methods]);
@@ -157,7 +164,8 @@ const RealitySchema = z.object({
});
function NestedDemo() {
const methods = useZodForm(RealitySchema, {
const methods = useForm<z.infer<typeof RealitySchema>>({
resolver: zodResolver(RealitySchema),
defaultValues: {
streamSettings: {
realitySettings: { dest: 'yahoo.com:443', serverNames: 'yahoo.com,www.yahoo.com' },
@@ -1,5 +1,4 @@
export { FormField, type FormFieldProps } from './FormField';
export { useZodForm } from './useZodForm';
export { rhfZodValidate } from './rhfZodValidate';
export { normalizeAntdOnChange, type ValueProp } from './normalizeAntdOnChange';
export { toDotted, type FieldName } from './toDotted';
@@ -1,20 +0,0 @@
import { useForm } from 'react-hook-form';
import type { FieldValues, Resolver, UseFormProps, UseFormReturn } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import type { z } from 'zod';
export function useZodForm<TFieldValues extends FieldValues>(
schema: z.ZodType<TFieldValues>,
options?: Omit<UseFormProps<TFieldValues>, 'resolver'>,
): UseFormReturn<TFieldValues> {
const resolver = zodResolver(
schema as z.ZodType<TFieldValues, TFieldValues>,
) as Resolver<TFieldValues>;
return useForm<TFieldValues>({
mode: 'onSubmit',
reValidateMode: 'onChange',
shouldUnregister: false,
...options,
resolver,
});
}