From f59c9f7b081a181683062d4a7b988c1dcf17b3c9 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 2 May 2024 02:19:18 +0530 Subject: [PATCH] chore: add loading spinners for all auth and onboarding form buttons. --- admin/app/login/components/sign-in-form.tsx | 18 +++++++-- admin/app/setup/components/sign-up-form.tsx | 40 +++++++++++++------ .../components/accounts/auth-forms/email.tsx | 10 ++--- .../accounts/auth-forms/password.tsx | 14 ++++--- .../accounts/auth-forms/unique-code.tsx | 23 ++++++----- space/components/accounts/onboarding-form.tsx | 15 ++----- web/components/account/auth-forms/email.tsx | 9 +++-- .../account/auth-forms/password.tsx | 19 ++++++--- .../account/auth-forms/sign-up-root.tsx | 2 +- .../account/auth-forms/unique-code.tsx | 23 ++++++----- .../onboarding/create-workspace.tsx | 8 ++-- web/components/onboarding/invitations.tsx | 4 +- web/components/onboarding/invite-members.tsx | 7 ++-- web/components/onboarding/profile-setup.tsx | 16 +++----- 14 files changed, 121 insertions(+), 87 deletions(-) diff --git a/admin/app/login/components/sign-in-form.tsx b/admin/app/login/components/sign-in-form.tsx index c28d0f27b..82254468a 100644 --- a/admin/app/login/components/sign-in-form.tsx +++ b/admin/app/login/components/sign-in-form.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation"; // services import { AuthService } from "@/services/auth.service"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; // components import { Banner } from "components/common"; // icons @@ -52,6 +52,7 @@ export const InstanceSignInForm: FC = (props) => { const [showPassword, setShowPassword] = useState(false); const [csrfToken, setCsrfToken] = useState(undefined); const [formData, setFormData] = useState(defaultFromData); + const [isSubmitting, setIsSubmitting] = useState(false); const handleFormChange = (key: keyof TFormData, value: string | boolean) => setFormData((prev) => ({ ...prev, [key]: value })); @@ -85,7 +86,10 @@ export const InstanceSignInForm: FC = (props) => { } else return { type: undefined, message: undefined }; }, [errorCode, errorMessage]); - const isButtonDisabled = useMemo(() => (formData.email && formData.password ? false : true), [formData]); + const isButtonDisabled = useMemo( + () => (!isSubmitting && formData.email && formData.password ? false : true), + [formData.email, formData.password, isSubmitting] + ); return (
@@ -97,7 +101,13 @@ export const InstanceSignInForm: FC = (props) => { {errorData.type && errorData?.message && } -
+ setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} + >
@@ -153,7 +163,7 @@ export const InstanceSignInForm: FC = (props) => {
diff --git a/admin/app/setup/components/sign-up-form.tsx b/admin/app/setup/components/sign-up-form.tsx index 8ef8add62..717159de6 100644 --- a/admin/app/setup/components/sign-up-form.tsx +++ b/admin/app/setup/components/sign-up-form.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation"; // services import { AuthService } from "@/services/auth.service"; // ui -import { Button, Checkbox, Input } from "@plane/ui"; +import { Button, Checkbox, Input, Spinner } from "@plane/ui"; // components import { Banner, PasswordStrengthMeter } from "components/common"; // icons @@ -68,6 +68,7 @@ export const InstanceSignUpForm: FC = (props) => { const [csrfToken, setCsrfToken] = useState(undefined); const [formData, setFormData] = useState(defaultFromData); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const handleFormChange = (key: keyof TFormData, value: string | boolean) => setFormData((prev) => ({ ...prev, [key]: value })); @@ -109,6 +110,7 @@ export const InstanceSignUpForm: FC = (props) => { const isButtonDisabled = useMemo( () => + !isSubmitting && formData.first_name && formData.email && formData.password && @@ -116,7 +118,7 @@ export const InstanceSignUpForm: FC = (props) => { formData.password === formData.confirm_password ? false : true, - [formData] + [formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting] ); return ( @@ -133,7 +135,13 @@ export const InstanceSignUpForm: FC = (props) => { )} -
+ setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} + >
@@ -252,25 +260,33 @@ export const InstanceSignUpForm: FC = (props) => { -
+
handleFormChange("confirm_password", e.target.value)} placeholder="Confirm password" - className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" + className={cn("w-full pr-10")} /> {showPassword ? ( - setShowPassword(false)} - /> + > + + ) : ( - setShowPassword(true)} - /> + > + + )}
{!!formData.confirm_password && formData.password !== formData.confirm_password && ( @@ -298,7 +314,7 @@ export const InstanceSignUpForm: FC = (props) => {
diff --git a/space/components/accounts/auth-forms/email.tsx b/space/components/accounts/auth-forms/email.tsx index 4a43e1fde..f79aba1f1 100644 --- a/space/components/accounts/auth-forms/email.tsx +++ b/space/components/accounts/auth-forms/email.tsx @@ -1,13 +1,13 @@ import React from "react"; import { Controller, useForm } from "react-hook-form"; +// types +import { IEmailCheckData } from "types/auth"; // icons import { XCircle, CircleAlert } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; // helpers import { checkEmailValidity } from "@/helpers/string.helper"; -// types -import { IEmailCheckData } from "types/auth"; type Props = { onSubmit: (data: IEmailCheckData) => Promise; @@ -84,8 +84,8 @@ export const EmailForm: React.FC = (props) => { )} />
- ); diff --git a/space/components/accounts/auth-forms/password.tsx b/space/components/accounts/auth-forms/password.tsx index 9ec087e4e..4530054d8 100644 --- a/space/components/accounts/auth-forms/password.tsx +++ b/space/components/accounts/auth-forms/password.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { Eye, EyeOff, XCircle } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accounts"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; @@ -40,6 +40,7 @@ export const PasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); const [csrfToken, setCsrfToken] = useState(undefined); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); // hooks const { instanceStore: { instance }, @@ -82,6 +83,7 @@ export const PasswordForm: React.FC = (props) => { const isButtonDisabled = useMemo( () => + !isSubmitting && !!passwordFormData.password && (mode === EAuthModes.SIGN_UP ? getPasswordStrength(passwordFormData.password) >= 3 && @@ -89,7 +91,7 @@ export const PasswordForm: React.FC = (props) => { : true) ? false : true, - [mode, passwordFormData] + [isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password] ); return ( @@ -97,6 +99,8 @@ export const PasswordForm: React.FC = (props) => { className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96" method="POST" action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`} + onSubmit={() => setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} > @@ -153,7 +157,7 @@ export const PasswordForm: React.FC = (props) => {
{passwordSupport}
- {mode === EAuthModes.SIGN_UP && getPasswordStrength(passwordFormData.password) >= 3 && ( + {mode === EAuthModes.SIGN_UP && (
diff --git a/space/components/accounts/auth-forms/unique-code.tsx b/space/components/accounts/auth-forms/unique-code.tsx index 525430bc6..0f647c7f5 100644 --- a/space/components/accounts/auth-forms/unique-code.tsx +++ b/space/components/accounts/auth-forms/unique-code.tsx @@ -8,7 +8,7 @@ import { IEmailCheckData } from "types/auth"; // icons import { CircleCheck, XCircle } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // services @@ -41,6 +41,7 @@ export const UniqueCodeForm: React.FC = (props) => { const [uniqueCodeFormData, setUniqueCodeFormData] = useState({ ...defaultValues, email }); const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); const [csrfToken, setCsrfToken] = useState(undefined); + const [isSubmitting, setIsSubmitting] = useState(false); // router const router = useRouter(); const { next_path } = router.query; @@ -99,12 +100,15 @@ export const UniqueCodeForm: React.FC = (props) => { }, []); const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; + const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting; return (
setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} > @@ -170,15 +174,14 @@ export const UniqueCodeForm: React.FC = (props) => { - ); diff --git a/space/components/accounts/onboarding-form.tsx b/space/components/accounts/onboarding-form.tsx index b43af9ffe..d1afe35c1 100644 --- a/space/components/accounts/onboarding-form.tsx +++ b/space/components/accounts/onboarding-form.tsx @@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form"; // types import { IUser } from "@plane/types"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // components import { UserImageUploadModal } from "@/components/accounts"; // hooks @@ -93,7 +93,7 @@ export const OnBoardingForm: React.FC = observer((props) => { }); }; - const isButtonDisabled = useMemo(() => (isValid ? false : true), [isValid]); + const isButtonDisabled = useMemo(() => (isValid && !isSubmitting ? false : true), [isSubmitting, isValid]); return (
@@ -202,15 +202,8 @@ export const OnBoardingForm: React.FC = observer((props) => { {errors.last_name && {errors.last_name.message}} - ); diff --git a/web/components/account/auth-forms/email.tsx b/web/components/account/auth-forms/email.tsx index 3110dd87b..f5225811c 100644 --- a/web/components/account/auth-forms/email.tsx +++ b/web/components/account/auth-forms/email.tsx @@ -5,7 +5,7 @@ import { CircleAlert, XCircle } from "lucide-react"; // types import { IEmailCheckData } from "@plane/types"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; // helpers import { checkEmailValidity } from "@/helpers/string.helper"; @@ -35,6 +35,8 @@ export const AuthEmailForm: FC = observer((props) => { setIsSubmitting(false); }; + const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting; + return (
@@ -72,10 +74,9 @@ export const AuthEmailForm: FC = observer((props) => { variant="primary" className="w-full" size="lg" - disabled={email.length === 0 || Boolean(emailError?.email)} - loading={isSubmitting} + disabled={isButtonDisabled} > - Continue + {isSubmitting ? : "Continue"} ); diff --git a/web/components/account/auth-forms/password.tsx b/web/components/account/auth-forms/password.tsx index 2bd08632c..9c4d2150e 100644 --- a/web/components/account/auth-forms/password.tsx +++ b/web/components/account/auth-forms/password.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; // icons import { Eye, EyeOff, XCircle } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, Spinner } from "@plane/ui"; // components import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account"; // constants @@ -45,6 +45,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { const [showPassword, setShowPassword] = useState(false); const [csrfToken, setCsrfToken] = useState(undefined); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); // hooks const { instance } = useInstance(); const { captureEvent } = useEventTracker(); @@ -84,6 +85,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { const isButtonDisabled = useMemo( () => + !isSubmitting && !!passwordFormData.password && (mode === EAuthModes.SIGN_UP ? getPasswordStrength(passwordFormData.password) >= 3 && @@ -91,7 +93,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { : true) ? false : true, - [mode, passwordFormData] + [isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password] ); return ( @@ -99,6 +101,8 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96" method="POST" action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`} + onSubmit={() => setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} >
@@ -189,7 +193,13 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { {mode === EAuthModes.SIGN_IN ? ( <> {instance && isSmtpConfigured && ( )}
); }); - diff --git a/web/components/account/auth-forms/sign-up-root.tsx b/web/components/account/auth-forms/sign-up-root.tsx index 55f3ffc99..25b996ae0 100644 --- a/web/components/account/auth-forms/sign-up-root.tsx +++ b/web/components/account/auth-forms/sign-up-root.tsx @@ -63,7 +63,7 @@ export const SignUpAuthRoot: FC = observer(() => { await authService .signUpEmailCheck(data) .then(() => { - if (isSmtpConfigured) setAuthStep(EAuthSteps.PASSWORD); + if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE); else setAuthStep(EAuthSteps.PASSWORD); }) .catch((error) => { diff --git a/web/components/account/auth-forms/unique-code.tsx b/web/components/account/auth-forms/unique-code.tsx index b0f6e6373..708f16216 100644 --- a/web/components/account/auth-forms/unique-code.tsx +++ b/web/components/account/auth-forms/unique-code.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { CircleCheck, XCircle } from "lucide-react"; import { IEmailCheckData } from "@plane/types"; -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { EAuthModes } from "@/helpers/authentication.helper"; import { API_BASE_URL } from "@/helpers/common.helper"; @@ -36,6 +36,7 @@ export const AuthUniqueCodeForm: React.FC = (props) => { const [uniqueCodeFormData, setUniqueCodeFormData] = useState({ ...defaultValues, email }); const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); const [csrfToken, setCsrfToken] = useState(undefined); + const [isSubmitting, setIsSubmitting] = useState(false); // store hooks // const { captureEvent } = useEventTracker(); // timer @@ -88,12 +89,15 @@ export const AuthUniqueCodeForm: React.FC = (props) => { }, []); const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; + const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting; return (
setIsSubmitting(true)} + onError={() => setIsSubmitting(false)} >
@@ -159,15 +163,14 @@ export const AuthUniqueCodeForm: React.FC = (props) => {
- ); diff --git a/web/components/onboarding/create-workspace.tsx b/web/components/onboarding/create-workspace.tsx index 5f6f60892..9550043c0 100644 --- a/web/components/onboarding/create-workspace.tsx +++ b/web/components/onboarding/create-workspace.tsx @@ -3,7 +3,7 @@ import { Controller, useForm } from "react-hook-form"; // types import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // ui -import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { WORKSPACE_CREATED } from "@/constants/event-tracker"; import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace"; @@ -116,6 +116,8 @@ export const CreateWorkspace: React.FC = (props) => { }); }; + const isButtonDisabled = !isValid || invalidSlug || isSubmitting; + return (
{!!invitedWorkspaces && ( @@ -256,8 +258,8 @@ export const CreateWorkspace: React.FC = (props) => { )}
- diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index a5fd5632f..e8ec3a48a 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -3,7 +3,7 @@ import useSWR from "swr";; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // ui -import { Button, Checkbox } from "@plane/ui"; +import { Button, Checkbox, Spinner } from "@plane/ui"; // constants import { MEMBER_ACCEPTED } from "@/constants/event-tracker"; import { USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys"; @@ -127,7 +127,7 @@ export const Invitations: React.FC = (props) => { onClick={submitInvitations} disabled={isJoiningWorkspaces || !invitationsRespond.length} > - Continue to workspace + {isJoiningWorkspaces ? : "Continue to workspace"}

diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index aa0a710ff..5b2b028c8 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -18,7 +18,7 @@ import { Listbox, Transition } from "@headlessui/react"; // types import { IUser, IWorkspace } from "@plane/types"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { MEMBER_INVITED } from "@/constants/event-tracker"; import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace"; @@ -420,10 +420,9 @@ export const InviteMembers: React.FC = (props) => { type="submit" size="lg" className="w-full" - disabled={isInvitationDisabled || !isValid} - loading={isSubmitting} + disabled={isInvitationDisabled || !isValid || isSubmitting} > - Continue + {isSubmitting ? : "Continue"}
)} -