From 7beed12eab14189f918fa8167f1a636e8ef9d36f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:18:27 +0800 Subject: [PATCH] refactor(web): migrate legacy forms to TanStack Form (#30631) --- .../base/form/hooks/use-get-form-values.ts | 2 +- web/app/components/base/form/utils/index.ts | 1 - .../base/form/utils/zod-submit-validator.ts | 22 ++ .../ForgotPasswordForm.spec.tsx | 163 +++++++++++ .../forgot-password/ForgotPasswordForm.tsx | 119 ++++---- web/app/install/installForm.spec.tsx | 158 +++++++++++ web/app/install/installForm.tsx | 255 ++++++++++-------- web/package.json | 2 - web/pnpm-lock.yaml | 31 --- 9 files changed, 551 insertions(+), 202 deletions(-) delete mode 100644 web/app/components/base/form/utils/index.ts create mode 100644 web/app/components/base/form/utils/zod-submit-validator.ts create mode 100644 web/app/forgot-password/ForgotPasswordForm.spec.tsx create mode 100644 web/app/install/installForm.spec.tsx diff --git a/web/app/components/base/form/hooks/use-get-form-values.ts b/web/app/components/base/form/hooks/use-get-form-values.ts index 9ea418ea00..3dd2eceb30 100644 --- a/web/app/components/base/form/hooks/use-get-form-values.ts +++ b/web/app/components/base/form/hooks/use-get-form-values.ts @@ -4,7 +4,7 @@ import type { GetValuesOptions, } from '../types' import { useCallback } from 'react' -import { getTransformedValuesWhenSecretInputPristine } from '../utils' +import { getTransformedValuesWhenSecretInputPristine } from '../utils/secret-input' import { useCheckValidated } from './use-check-validated' export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) => { diff --git a/web/app/components/base/form/utils/index.ts b/web/app/components/base/form/utils/index.ts deleted file mode 100644 index 0abb8d1ad5..0000000000 --- a/web/app/components/base/form/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './secret-input' diff --git a/web/app/components/base/form/utils/zod-submit-validator.ts b/web/app/components/base/form/utils/zod-submit-validator.ts new file mode 100644 index 0000000000..23eacaf8a4 --- /dev/null +++ b/web/app/components/base/form/utils/zod-submit-validator.ts @@ -0,0 +1,22 @@ +import type { ZodSchema } from 'zod' + +type SubmitValidator = ({ value }: { value: T }) => { fields: Record } | undefined + +export const zodSubmitValidator = (schema: ZodSchema): SubmitValidator => { + return ({ value }) => { + const result = schema.safeParse(value) + if (!result.success) { + const fieldErrors: Record = {} + for (const issue of result.error.issues) { + const path = issue.path[0] + if (path === undefined) + continue + const key = String(path) + if (!fieldErrors[key]) + fieldErrors[key] = issue.message + } + return { fields: fieldErrors } + } + return undefined + } +} diff --git a/web/app/forgot-password/ForgotPasswordForm.spec.tsx b/web/app/forgot-password/ForgotPasswordForm.spec.tsx new file mode 100644 index 0000000000..aa360cb6c3 --- /dev/null +++ b/web/app/forgot-password/ForgotPasswordForm.spec.tsx @@ -0,0 +1,163 @@ +import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fetchInitValidateStatus, fetchSetupStatus, sendForgotPasswordEmail } from '@/service/common' +import ForgotPasswordForm from './ForgotPasswordForm' + +const mockPush = vi.fn() + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: mockPush }), +})) + +vi.mock('@/service/common', () => ({ + fetchSetupStatus: vi.fn(), + fetchInitValidateStatus: vi.fn(), + sendForgotPasswordEmail: vi.fn(), +})) + +const mockFetchSetupStatus = vi.mocked(fetchSetupStatus) +const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus) +const mockSendForgotPasswordEmail = vi.mocked(sendForgotPasswordEmail) + +const prepareLoadedState = () => { + mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse) + mockFetchInitValidateStatus.mockResolvedValue({ status: 'finished' } as InitValidateStatusResponse) +} + +describe('ForgotPasswordForm', () => { + beforeEach(() => { + vi.clearAllMocks() + prepareLoadedState() + }) + + it('should render form after loading', async () => { + render() + + expect(await screen.findByLabelText('login.email')).toBeInTheDocument() + }) + + it('should show validation error when email is empty', async () => { + render() + + await screen.findByLabelText('login.email') + + fireEvent.click(screen.getByRole('button', { name: /login\.sendResetLink/ })) + + await waitFor(() => { + expect(screen.getByText('login.error.emailInValid')).toBeInTheDocument() + }) + expect(mockSendForgotPasswordEmail).not.toHaveBeenCalled() + }) + + it('should send reset email and navigate after confirmation', async () => { + mockSendForgotPasswordEmail.mockResolvedValue({ result: 'success', data: 'ok' } as any) + + render() + + const emailInput = await screen.findByLabelText('login.email') + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + + fireEvent.click(screen.getByRole('button', { name: /login\.sendResetLink/ })) + + await waitFor(() => { + expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({ + url: '/forgot-password', + body: { email: 'test@example.com' }, + }) + }) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /login\.backToSignIn/ })).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /login\.backToSignIn/ })) + expect(mockPush).toHaveBeenCalledWith('/signin') + }) + + it('should submit when form is submitted', async () => { + mockSendForgotPasswordEmail.mockResolvedValue({ result: 'success', data: 'ok' } as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'test@example.com' } }) + + const form = screen.getByRole('button', { name: /login\.sendResetLink/ }).closest('form') + expect(form).not.toBeNull() + + fireEvent.submit(form as HTMLFormElement) + + await waitFor(() => { + expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({ + url: '/forgot-password', + body: { email: 'test@example.com' }, + }) + }) + }) + + it('should disable submit while request is in flight', async () => { + let resolveRequest: ((value: any) => void) | undefined + const requestPromise = new Promise((resolve) => { + resolveRequest = resolve + }) + mockSendForgotPasswordEmail.mockReturnValue(requestPromise as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'test@example.com' } }) + + const button = screen.getByRole('button', { name: /login\.sendResetLink/ }) + fireEvent.click(button) + + await waitFor(() => { + expect(button).toBeDisabled() + }) + + fireEvent.click(button) + expect(mockSendForgotPasswordEmail).toHaveBeenCalledTimes(1) + + resolveRequest?.({ result: 'success', data: 'ok' }) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /login\.backToSignIn/ })).toBeInTheDocument() + }) + }) + + it('should keep form state when request fails', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockSendForgotPasswordEmail.mockResolvedValue({ result: 'fail', data: 'error' } as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'test@example.com' } }) + fireEvent.click(screen.getByRole('button', { name: /login\.sendResetLink/ })) + + await waitFor(() => { + expect(mockSendForgotPasswordEmail).toHaveBeenCalledTimes(1) + }) + + expect(screen.getByRole('button', { name: /login\.sendResetLink/ })).toBeInTheDocument() + expect(mockPush).not.toHaveBeenCalled() + + consoleSpy.mockRestore() + }) + + it('should redirect to init when status is not started', async () => { + const originalLocation = window.location + Object.defineProperty(window, 'location', { + value: { href: '' }, + writable: true, + }) + mockFetchInitValidateStatus.mockResolvedValue({ status: 'not_started' } as InitValidateStatusResponse) + + render() + + await waitFor(() => { + expect(window.location.href).toBe('/init') + }) + + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + }) + }) +}) diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index 7299d24ebc..ff33cccc82 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -1,15 +1,16 @@ 'use client' import type { InitValidateStatusResponse } from '@/models/common' -import { zodResolver } from '@hookform/resolvers/zod' +import { useStore } from '@tanstack/react-form' import { useRouter } from 'next/navigation' import * as React from 'react' import { useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { z } from 'zod' import Button from '@/app/components/base/button' +import { formContext, useAppForm } from '@/app/components/base/form' +import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator' import { fetchInitValidateStatus, fetchSetupStatus, @@ -27,44 +28,45 @@ const accountFormSchema = z.object({ .email('error.emailInValid'), }) -type AccountFormValues = z.infer - const ForgotPasswordForm = () => { const { t } = useTranslation() const router = useRouter() const [loading, setLoading] = useState(true) const [isEmailSent, setIsEmailSent] = useState(false) - const { register, trigger, getValues, formState: { errors } } = useForm({ - resolver: zodResolver(accountFormSchema), + + const form = useAppForm({ defaultValues: { email: '' }, + validators: { + onSubmit: zodSubmitValidator(accountFormSchema), + }, + onSubmit: async ({ value }) => { + try { + const res = await sendForgotPasswordEmail({ + url: '/forgot-password', + body: { email: value.email }, + }) + if (res.result === 'success') + setIsEmailSent(true) + else console.error('Email verification failed') + } + catch (error) { + console.error('Request failed:', error) + } + }, }) - const handleSendResetPasswordEmail = async (email: string) => { - try { - const res = await sendForgotPasswordEmail({ - url: '/forgot-password', - body: { email }, - }) - if (res.result === 'success') - setIsEmailSent(true) - - else console.error('Email verification failed') - } - catch (error) { - console.error('Request failed:', error) - } - } + const isSubmitting = useStore(form.store, state => state.isSubmitting) + const emailErrors = useStore(form.store, state => state.fieldMeta.email?.errors) const handleSendResetPasswordClick = async () => { + if (isSubmitting) + return + if (isEmailSent) { router.push('/signin') } else { - const isValid = await trigger('email') - if (isValid) { - const email = getValues('email') - await handleSendResetPasswordEmail(email) - } + form.handleSubmit() } } @@ -94,30 +96,51 @@ const ForgotPasswordForm = () => {
-
- {!isEmailSent && ( -
- -
- - {errors.email && {t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}} + + { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + {!isEmailSent && ( +
+ +
+ + {field => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder={t('emailPlaceholder', { ns: 'login' }) || ''} + /> + )} + + {emailErrors && emailErrors.length > 0 && ( + + {t(`${emailErrors[0]}` as 'error.emailInValid', { ns: 'login' })} + + )} +
+ )} +
+
- )} -
- -
- + +
diff --git a/web/app/install/installForm.spec.tsx b/web/app/install/installForm.spec.tsx new file mode 100644 index 0000000000..74602f916a --- /dev/null +++ b/web/app/install/installForm.spec.tsx @@ -0,0 +1,158 @@ +import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common' +import { encryptPassword } from '@/utils/encryption' +import InstallForm from './installForm' + +const mockPush = vi.fn() +const mockReplace = vi.fn() + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: mockPush, replace: mockReplace }), +})) + +vi.mock('@/service/common', () => ({ + fetchSetupStatus: vi.fn(), + fetchInitValidateStatus: vi.fn(), + setup: vi.fn(), + login: vi.fn(), + getSystemFeatures: vi.fn(), +})) + +const mockFetchSetupStatus = vi.mocked(fetchSetupStatus) +const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus) +const mockSetup = vi.mocked(setup) +const mockLogin = vi.mocked(login) + +const prepareLoadedState = () => { + mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse) + mockFetchInitValidateStatus.mockResolvedValue({ status: 'finished' } as InitValidateStatusResponse) +} + +describe('InstallForm', () => { + beforeEach(() => { + vi.clearAllMocks() + prepareLoadedState() + }) + + it('should render form after loading', async () => { + render() + + expect(await screen.findByLabelText('login.email')).toBeInTheDocument() + expect(screen.getByRole('button', { name: /login\.installBtn/ })).toBeInTheDocument() + }) + + it('should show validation error when required fields are empty', async () => { + render() + + await screen.findByLabelText('login.email') + + fireEvent.click(screen.getByRole('button', { name: /login\.installBtn/ })) + + await waitFor(() => { + expect(screen.getByText('login.error.emailInValid')).toBeInTheDocument() + expect(screen.getByText('login.error.nameEmpty')).toBeInTheDocument() + }) + expect(mockSetup).not.toHaveBeenCalled() + }) + + it('should submit and redirect to apps on successful login', async () => { + mockSetup.mockResolvedValue({ result: 'success' } as any) + mockLogin.mockResolvedValue({ result: 'success', data: { access_token: 'token' } } as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'admin@example.com' } }) + fireEvent.change(screen.getByLabelText('login.name'), { target: { value: 'Admin' } }) + fireEvent.change(screen.getByLabelText('login.password'), { target: { value: 'Password123' } }) + + const form = screen.getByRole('button', { name: /login\.installBtn/ }).closest('form') + expect(form).not.toBeNull() + + fireEvent.submit(form as HTMLFormElement) + + await waitFor(() => { + expect(mockSetup).toHaveBeenCalledWith({ + body: { + email: 'admin@example.com', + name: 'Admin', + password: 'Password123', + language: 'en', + }, + }) + }) + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledWith({ + url: '/login', + body: { + email: 'admin@example.com', + password: encryptPassword('Password123'), + }, + }) + }) + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/apps') + }) + }) + + it('should redirect to sign in when login fails', async () => { + mockSetup.mockResolvedValue({ result: 'success' } as any) + mockLogin.mockResolvedValue({ result: 'fail', data: 'error', code: 'login_failed', message: 'login failed' } as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'admin@example.com' } }) + fireEvent.change(screen.getByLabelText('login.name'), { target: { value: 'Admin' } }) + fireEvent.change(screen.getByLabelText('login.password'), { target: { value: 'Password123' } }) + + fireEvent.click(screen.getByRole('button', { name: /login\.installBtn/ })) + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/signin') + }) + }) + + it('should disable submit while request is in flight', async () => { + let resolveSetup: ((value: any) => void) | undefined + const setupPromise = new Promise((resolve) => { + resolveSetup = resolve + }) + mockSetup.mockReturnValue(setupPromise as any) + mockLogin.mockResolvedValue({ result: 'success', data: { access_token: 'token' } } as any) + + render() + + fireEvent.change(await screen.findByLabelText('login.email'), { target: { value: 'admin@example.com' } }) + fireEvent.change(screen.getByLabelText('login.name'), { target: { value: 'Admin' } }) + fireEvent.change(screen.getByLabelText('login.password'), { target: { value: 'Password123' } }) + + const button = screen.getByRole('button', { name: /login\.installBtn/ }) + fireEvent.click(button) + + await waitFor(() => { + expect(button).toBeDisabled() + }) + + fireEvent.click(button) + expect(mockSetup).toHaveBeenCalledTimes(1) + + resolveSetup?.({ result: 'success' }) + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledTimes(1) + }) + }) + + it('should redirect to sign in when setup is finished', async () => { + mockFetchSetupStatus.mockResolvedValue({ step: 'finished' } as SetupStatusResponse) + + render() + + await waitFor(() => { + expect(localStorage.setItem).toHaveBeenCalledWith('setup_status', 'finished') + expect(mockPush).toHaveBeenCalledWith('/signin') + }) + }) +}) diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index c43fbb4251..de32f18bc7 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -1,18 +1,17 @@ 'use client' -import type { SubmitHandler } from 'react-hook-form' import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' -import { zodResolver } from '@hookform/resolvers/zod' - -import { useDebounceFn } from 'ahooks' +import { useStore } from '@tanstack/react-form' import Link from 'next/link' import { useRouter } from 'next/navigation' import * as React from 'react' -import { useCallback, useEffect } from 'react' -import { useForm } from 'react-hook-form' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { z } from 'zod' import Button from '@/app/components/base/button' +import { formContext, useAppForm } from '@/app/components/base/form' +import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator' +import Input from '@/app/components/base/input' import { validPassword } from '@/config' import { useDocLink } from '@/context/i18n' @@ -33,8 +32,6 @@ const accountFormSchema = z.object({ }).regex(validPassword, 'error.passwordInvalid'), }) -type AccountFormValues = z.infer - const InstallForm = () => { useDocumentTitle('') const { t, i18n } = useTranslation() @@ -42,64 +39,49 @@ const InstallForm = () => { const router = useRouter() const [showPassword, setShowPassword] = React.useState(false) const [loading, setLoading] = React.useState(true) - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: zodResolver(accountFormSchema), + + const form = useAppForm({ defaultValues: { name: '', password: '', email: '', }, - }) + validators: { + onSubmit: zodSubmitValidator(accountFormSchema), + }, + onSubmit: async ({ value }) => { + // First, setup the admin account + await setup({ + body: { + ...value, + language: i18n.language, + }, + }) - const onSubmit: SubmitHandler = async (data) => { - // First, setup the admin account - await setup({ - body: { - ...data, - language: i18n.language, - }, - }) + // Then, automatically login with the same credentials + const loginRes = await login({ + url: '/login', + body: { + email: value.email, + password: encodePassword(value.password), + }, + }) - // Then, automatically login with the same credentials - const loginRes = await login({ - url: '/login', - body: { - email: data.email, - password: encodePassword(data.password), - }, - }) - - // Store tokens and redirect to apps if login successful - if (loginRes.result === 'success') { - router.replace('/apps') - } - else { - // Fallback to signin page if auto-login fails - router.replace('/signin') - } - } - - const handleSetting = async () => { - if (isSubmitting) - return - handleSubmit(onSubmit)() - } - - const { run: debouncedHandleKeyDown } = useDebounceFn( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSetting() + // Store tokens and redirect to apps if login successful + if (loginRes.result === 'success') { + router.replace('/apps') + } + else { + // Fallback to signin page if auto-login fails + router.replace('/signin') } }, - { wait: 200 }, - ) + }) - const handleKeyDown = useCallback(debouncedHandleKeyDown, [debouncedHandleKeyDown]) + const isSubmitting = useStore(form.store, state => state.isSubmitting) + const emailErrors = useStore(form.store, state => state.fieldMeta.email?.errors) + const nameErrors = useStore(form.store, state => state.fieldMeta.name?.errors) + const passwordErrors = useStore(form.store, state => state.fieldMeta.password?.errors) useEffect(() => { fetchSetupStatus().then((res: SetupStatusResponse) => { @@ -128,76 +110,111 @@ const InstallForm = () => {
-
-
- -
- - {errors.email && {t(`${errors.email?.message}` as 'error.emailInValid', { ns: 'login' })}} -
- -
- -
- -
- -
- {errors.name && {t(`${errors.name.message}` as 'error.nameEmpty', { ns: 'login' })}} -
- -
- -
- - -
- + + { + e.preventDefault() + e.stopPropagation() + if (isSubmitting) + return + form.handleSubmit() + }} + > +
+ +
+ + {field => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder={t('emailPlaceholder', { ns: 'login' }) || ''} + /> + )} + + {emailErrors && emailErrors.length > 0 && ( + + {t(`${emailErrors[0]}` as 'error.emailInValid', { ns: 'login' })} + + )}
-
- {t('error.passwordInvalid', { ns: 'login' })} +
+ +
+ + {field => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder={t('namePlaceholder', { ns: 'login' }) || ''} + /> + )} + +
+ {nameErrors && nameErrors.length > 0 && ( + + {t(`${nameErrors[0]}` as 'error.nameEmpty', { ns: 'login' })} + + )}
-
-
- -
- +
+ +
+ + {field => ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''} + /> + )} + + +
+ +
+
+ +
0, + })} + > + {t('error.passwordInvalid', { ns: 'login' })} +
+
+ +
+ +
+ +
{t('license.tip', { ns: 'login' })} -   +   = 16 || ^19.0.0-rc' - '@hookform/resolvers@5.2.2': - resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} - peerDependencies: - react-hook-form: ^7.55.0 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -3211,9 +3200,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@storybook/addon-docs@9.1.13': resolution: {integrity: sha512-V1nCo7bfC3kQ5VNVq0VDcHsIhQf507m+BxMA5SIYiwdJHljH2BXpW2fL3FFn9gv9Wp57AEEzhm+wh4zANaJgkg==} peerDependencies: @@ -7436,12 +7422,6 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.68.0: - resolution: {integrity: sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 || ^19 - react-hotkeys-hook@4.6.2: resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==} peerDependencies: @@ -10516,11 +10496,6 @@ snapshots: dependencies: react: 19.2.3 - '@hookform/resolvers@5.2.2(react-hook-form@7.68.0(react@19.2.3))': - dependencies: - '@standard-schema/utils': 0.3.0 - react-hook-form: 7.68.0(react@19.2.3) - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -11782,8 +11757,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@standard-schema/utils@0.3.0': {} - '@storybook/addon-docs@9.1.13(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) @@ -16931,10 +16904,6 @@ snapshots: react-fast-compare@3.2.2: {} - react-hook-form@7.68.0(react@19.2.3): - dependencies: - react: 19.2.3 - react-hotkeys-hook@4.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3