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, useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { FormField } from './FormField'; const GB = 1024 * 1024 * 1024; const meta = { title: 'Form/RHF/FormField', component: FormField, tags: ['autodocs'], parameters: { layout: 'padded', docs: { description: { component: 'Bridges one Ant Design control into react-hook-form: wraps the child in a Controller plus Form.Item, normalizes the onChange payload, and surfaces resolver errors as translated help text. Every RHF panel form (client, inbound, outbound, and host modals) builds its fields with it.', }, }, }, argTypes: { name: { description: 'Field path — a dotted string or an array of segments joined with dots.' }, control: { description: 'Optional react-hook-form control; falls back to the surrounding FormProvider.', }, label: { description: 'Form.Item label.' }, tooltip: { description: 'Form.Item tooltip shown next to the label.' }, extra: { description: 'Helper text rendered below the input.' }, valueProp: { description: 'Prop the child receives the value on: `value` (default) or `checked` for switches.', }, transform: { description: 'Optional input/output mappers, e.g. bytes stored in the form but GB shown in the input.', }, onAfterChange: { description: 'Called with the stored value after every change.' }, rules: { description: 'Controller-level validation rules applied on top of the form resolver.', }, required: { description: 'Marks the label with the required asterisk.' }, noStyle: { description: 'Render the bare input without Form.Item chrome.' }, children: { description: 'The single Ant Design control to wire up.' }, }, } satisfies Meta; export default meta; type Story = StoryObj; const ClientSchema = z.object({ email: z.string(), flow: z.string(), enable: z.boolean(), }); function ClientDemo() { const methods = useForm>({ resolver: zodResolver(ClientSchema), defaultValues: { email: 'user1@example.com', flow: 'xtls-rprx-vision', enable: true }, }); return (
); } const RealitySchema = z.object({ streamSettings: z.object({ realitySettings: z.object({ dest: z.string(), serverNames: z.string(), }), }), }); function NestedDemo() { const methods = useForm>({ resolver: zodResolver(RealitySchema), defaultValues: { streamSettings: { realitySettings: { dest: 'yahoo.com:443', serverNames: 'yahoo.com,www.yahoo.com' }, }, }, }); return (
); } const placeholderArgs = { name: 'email', children: , }; export const ClientFields: Story = { args: placeholderArgs, render: () => , }; export const TrafficTransform: Story = { args: placeholderArgs, render: () => , play: async ({ canvas, userEvent }) => { const input = canvas.getByRole('spinbutton'); await userEvent.clear(input); await userEvent.type(input, '100'); await expect(await canvas.findByText(/107,374,182,400 bytes/)).toBeVisible(); }, }; export const ValidationErrors: Story = { args: placeholderArgs, render: () => , play: async ({ canvas, userEvent }) => { const remarkError = await canvas.findByText('Remark is required'); await waitFor(() => expect(remarkError).toBeVisible()); await waitFor(() => expect(canvas.getByText('Port must be between 1 and 65535')).toBeVisible()); await userEvent.type(canvas.getByPlaceholderText('vless-reality-443'), 'vless-reality-443'); await userEvent.click(canvas.getByRole('button', { name: 'Validate' })); await waitFor(() => expect(canvas.queryByText('Remark is required')).not.toBeInTheDocument()); await expect(canvas.getByText('Port must be between 1 and 65535')).toBeVisible(); }, }; export const NestedNames: Story = { args: placeholderArgs, render: () => , };