From 3801073fd7bd341d138404b3e3276d0d00018560 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Wed, 24 Sep 2025 10:45:45 +0330 Subject: [PATCH] feat: Add loading states and improve form handling This commit introduces several enhancements to form components and user experience across the application. - **Sign-in Form:** - Implemented a loading state using `useIsMutating` from TanStack Query. - The username and password fields are now disabled during the login process to prevent concurrent submissions. - The "Sign in" button displays a spinner while the mutation is pending, providing clear visual feedback. - **Assign to Company Modal:** - Refactored the modal to use `react-hook-form` for more robust state management and validation. - The form submission button is now disabled while the assignment request is in progress. - **Tag Input Component:** - Added a new `shouldAddWithSpace` prop to allow creating tags by pressing the space bar, increasing the component's flexibility. - **Header:** - Corrected the image path for the mobile header logo to display the correct icon. --- src/components/Auth/SigninWithPassword.tsx | 21 ++++++++----- .../FormElements/InputGroup/tag-input.tsx | 10 +++++-- src/components/Layouts/header/index.tsx | 2 +- .../customers/AssignToCompanyModalContent.tsx | 30 +++++++++---------- src/components/Tables/customers/index.tsx | 8 ++++- .../customers/useCustomerListPresenter.ts | 6 ++-- .../forms/companies/CreateCompany.tsx | 1 + .../forms/customers/CreateCustomer.tsx | 3 +- src/constants/regex.ts | 1 + src/hooks/queries/useUsersQueries.ts | 9 +++--- 10 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/components/Auth/SigninWithPassword.tsx b/src/components/Auth/SigninWithPassword.tsx index 211cb40..f8b86ea 100644 --- a/src/components/Auth/SigninWithPassword.tsx +++ b/src/components/Auth/SigninWithPassword.tsx @@ -1,30 +1,32 @@ "use client"; import { PasswordIcon, UserIcon } from "@/assets/icons"; -import InputGroup from "../FormElements/InputGroup"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { ILoginCredentials } from "@/lib/api"; import { useLoginQuery } from "@/hooks/queries"; -import { useRef, useState } from "react"; +import { ILoginCredentials } from "@/lib/api"; +import { useState } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import InputGroup from "../FormElements/InputGroup"; import { FormErrorMessages } from "@/constants/Texts"; +import { useIsMutating } from "@tanstack/react-query"; export default function SigninWithPassword() { + const isMutating = useIsMutating(); const [passwordFieldInputType, setPasswordFieldInputType] = useState< "password" | "text" >("password"); const { handleSubmit, control, reset, register } = useForm(); - const { mutate, isPending } = useLoginQuery(); + const { mutate } = useLoginQuery(reset); const onSubmit: SubmitHandler = (data) => { mutate(data); - reset(); }; return (
- Sign In + {isMutating ? ( +
+ ) : ( + "Sign in" + )} diff --git a/src/components/FormElements/InputGroup/tag-input.tsx b/src/components/FormElements/InputGroup/tag-input.tsx index 084f2cb..ade2004 100644 --- a/src/components/FormElements/InputGroup/tag-input.tsx +++ b/src/components/FormElements/InputGroup/tag-input.tsx @@ -28,6 +28,7 @@ type TagInputProps = { watch?: UseFormWatch; errors?: FieldErrors; maxTags?: number; + shouldAddWithSpace?: boolean; allowDuplicates?: boolean; tagValidator?: (tag: string) => boolean; } & Partial; @@ -43,6 +44,7 @@ const TagInput = ({ name, register, setValue, + shouldAddWithSpace = false, watch, errors, maxTags, @@ -87,7 +89,11 @@ const TagInput = ({ }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === ",") { + if ( + e.key === "Enter" || + e.key === "," || + (shouldAddWithSpace && e.key === " ") + ) { e.preventDefault(); addTag(inputValue); } else if (e.key === "Backspace" && !inputValue && tags.length > 0) { @@ -167,7 +173,7 @@ const TagInput = ({ 0 ? JSON.stringify(tags) : ''} + value={tags.length > 0 ? JSON.stringify(tags) : ""} ref={ref} /> diff --git a/src/components/Layouts/header/index.tsx b/src/components/Layouts/header/index.tsx index da417b0..a7a7dd3 100644 --- a/src/components/Layouts/header/index.tsx +++ b/src/components/Layouts/header/index.tsx @@ -24,7 +24,7 @@ export function Header() { {isMobile && ( + SetStateAction >; + defaultValues?: TAssignCustomerToCompanyCredentials; } -const AssignToCompanyModalContent: FC = ({ setCompanies }) => { +const AssignToCompanyModalContent: FC = ({ + setCompanies, + defaultValues, +}) => { const [options, setOptions] = useState([]); const { data, status, isSuccess } = useCompanyFullList(); const { handleSubmit, register } = - useForm(); + useForm({ + mode: "onChange", + defaultValues, + }); useEffect(() => { if (isSuccess) { diff --git a/src/components/Tables/customers/index.tsx b/src/components/Tables/customers/index.tsx index 9a33053..0d0d4da 100644 --- a/src/components/Tables/customers/index.tsx +++ b/src/components/Tables/customers/index.tsx @@ -112,6 +112,9 @@ const CustomersTable = () => { onClick={() => { setCurrentCustomer(customer); setIsAssignCompanyModalOpen(true); + setSelectedCompanies({ + company_ids: [customer?.companies?.[0]?.id ?? ""], + }); }} > Assign To Company @@ -145,7 +148,10 @@ const CustomersTable = () => { isOpen={isAssignCompanyModalOpen} onClose={() => setIsAssignCompanyModalOpen(false)} > - + {isModalOpen && ( { const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] = useState(false); - const [selectedCompanies, setSelectedCompanies] = - useState(null); + const [selectedCompanies, setSelectedCompanies] = useState< + TAssignCustomerToCompanyCredentials | undefined + >(); const [currentCustomer, setCurrentCustomer] = useState( null, ); @@ -50,6 +51,7 @@ const useCustomerListPresenter = () => { deleteCustomer(currentCustomer.id); } }; + const customers = data?.pages.flatMap((page) => page.data.customers) ?? []; return { allCustomers: customers, diff --git a/src/components/forms/companies/CreateCompany.tsx b/src/components/forms/companies/CreateCompany.tsx index 51c8992..87e9572 100644 --- a/src/components/forms/companies/CreateCompany.tsx +++ b/src/components/forms/companies/CreateCompany.tsx @@ -497,6 +497,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => { {...register("tags.cpv_codes")} label="CPV codes" name="tags.cpv_codes" + shouldAddWithSpace watch={watch} setValue={setValue} placeholder="Enter Tag CPV codes" diff --git a/src/components/forms/customers/CreateCustomer.tsx b/src/components/forms/customers/CreateCustomer.tsx index a519cba..3371748 100644 --- a/src/components/forms/customers/CreateCustomer.tsx +++ b/src/components/forms/customers/CreateCustomer.tsx @@ -6,6 +6,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect"; import { Select } from "@/components/FormElements/select"; import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import FormFooter from "@/components/ui/FormFooter"; +import { USERNAME_REGEX } from "@/constants/regex"; import { FormErrorMessages } from "@/constants/Texts"; import { CreateCustomerCredentials } from "@/lib/api"; import useCreateCustomerPresenter from "./useCreateCustomerPresenter"; @@ -36,7 +37,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => { message: FormErrorMessages.maxLength(30), }, pattern: { - value: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/, + value: USERNAME_REGEX, message: FormErrorMessages.invalidPattern, }, })} diff --git a/src/constants/regex.ts b/src/constants/regex.ts index 72881aa..90e6dcb 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -1 +1,2 @@ export const PHONE_REGEX = /^[+\d]+$/; +export const USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/; diff --git a/src/hooks/queries/useUsersQueries.ts b/src/hooks/queries/useUsersQueries.ts index f8ac8c1..602098f 100644 --- a/src/hooks/queries/useUsersQueries.ts +++ b/src/hooks/queries/useUsersQueries.ts @@ -10,7 +10,7 @@ import { useRouter } from "next/navigation"; import { useMemo } from "react"; import { toast } from "react-toastify"; -export const useLoginQuery = () => { +export const useLoginQuery = (callback?: () => void) => { const mutationKey = useMemo(() => [API_ENDPOINTS.USER.LOGIN], []); const { setAuthState } = useUser(); const router = useRouter(); @@ -18,9 +18,10 @@ export const useLoginQuery = () => { mutationKey, mutationFn: userService.login, onSuccess: (response) => { - if (response.success) { - setAuthState(response.data); - router.replace("/charts"); + setAuthState(response.data); + router.replace("/charts"); + if (callback) { + callback(); } }, });