From e15839bab1ea667e4a344ba89c3421b2eb0ffc0b Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Mon, 18 May 2026 03:38:12 +0700 Subject: [PATCH] feat(frontend): add FormSubmitButton and apply it to three forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New shared component src/components/ui/form-submit-button.tsx. It is the react-hook-form-flavoured analogue of React 19's useFormStatus: the button subscribes to the nearest FormProvider via useFormContext and reads formState.isSubmitting / isValid / isSubmitted itself, so the surrounding form no longer has to thread loading flags down. Shows a Loader2 spinner while submitting and disables itself once the form is dirty-and-invalid (opt out with requireValid={false}). Applied to the three forms where the submit is a plain + ); +} + +export { FormSubmitButton }; diff --git a/frontend/src/features/authentication/login-form.tsx b/frontend/src/features/authentication/login-form.tsx index 98e6ac96..7bbb2ea2 100644 --- a/frontend/src/features/authentication/login-form.tsx +++ b/frontend/src/features/authentication/login-form.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader2 } from 'lucide-react'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; @@ -11,6 +10,7 @@ import Github from '@/components/icons/github'; import Google from '@/components/icons/google'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { FormSubmitButton } from '@/components/ui/form-submit-button'; import { Input } from '@/components/ui/input'; import { useUser } from '@/providers/user-provider'; @@ -76,7 +76,6 @@ function LoginForm({ providers, returnUrl = '/flows/new' }: LoginFormProps) { const handleSubmit = async (values: z.infer) => { setError(null); - setIsSubmitting(true); try { const result = await login(values); @@ -96,8 +95,6 @@ function LoginForm({ providers, returnUrl = '/flows/new' }: LoginFormProps) { navigate(returnUrl); } catch { setError(errorMessage); - } finally { - setIsSubmitting(false); } }; @@ -188,7 +185,7 @@ function LoginForm({ providers, returnUrl = '/flows/new' }: LoginFormProps) { .filter((provider) => providers.includes(provider.id)) .map((provider) => ( + {error && {error}} diff --git a/frontend/src/features/authentication/password-change-form.tsx b/frontend/src/features/authentication/password-change-form.tsx index fe12f3cb..30392a4f 100644 --- a/frontend/src/features/authentication/password-change-form.tsx +++ b/frontend/src/features/authentication/password-change-form.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { Eye, EyeOff, Loader2 } from 'lucide-react'; +import { Eye, EyeOff } from 'lucide-react'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; @@ -7,6 +7,7 @@ import * as z from 'zod'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { FormSubmitButton } from '@/components/ui/form-submit-button'; import { Input } from '@/components/ui/input'; import { api, type ApiErrorResponse, type ApiHttpError } from '@/lib/axios'; @@ -64,7 +65,6 @@ export function PasswordChangeForm({ onSuccess, showSkip = false, }: PasswordChangeFormProps) { - const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); @@ -80,7 +80,6 @@ export function PasswordChangeForm({ }); const handleSubmit = async (values: PasswordChangeFormValues) => { - setIsSubmitting(true); setError(null); try { @@ -133,8 +132,6 @@ export function PasswordChangeForm({ } setError(errorMessage); - } finally { - setIsSubmitting(false); } }; @@ -272,13 +269,9 @@ export function PasswordChangeForm({ Cancel )} - + diff --git a/frontend/src/features/resources/resources-mkdir-dialog.tsx b/frontend/src/features/resources/resources-mkdir-dialog.tsx index 71c82ff0..64ece8d9 100644 --- a/frontend/src/features/resources/resources-mkdir-dialog.tsx +++ b/frontend/src/features/resources/resources-mkdir-dialog.tsx @@ -1,11 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { FolderPlus, Loader2 } from 'lucide-react'; +import { FolderPlus } from 'lucide-react'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { FormSubmitButton } from '@/components/ui/form-submit-button'; import { Input } from '@/components/ui/input'; import { resourcesMkdirFormSchema, type ResourcesMkdirFormValues, useResourcesMkdir } from './use-resources-mkdir'; @@ -68,8 +69,6 @@ function ResourcesMkdirDialogForm({ defaultParentPath, onClose }: ResourcesMkdir } }); - const isSubmitDisabled = !form.formState.isValid || isCreating; - return ( @@ -120,13 +119,10 @@ function ResourcesMkdirDialogForm({ defaultParentPath, onClose }: ResourcesMkdir > Cancel - +