diff --git a/web/components/onboarding/profile-setup.tsx b/web/components/onboarding/profile-setup.tsx index 2a8f64f1e..1c619a8ac 100644 --- a/web/components/onboarding/profile-setup.tsx +++ b/web/components/onboarding/profile-setup.tsx @@ -24,6 +24,8 @@ import { FileService } from "@/services/file.service"; // assets import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg"; import ProfileSetupLight from "public/onboarding/profile-setup-light.svg"; +import UserPersonalizationDark from "public/onboarding/user-personalization-dark.svg"; +import UserPersonalizationLight from "public/onboarding/user-personalization-light.svg"; type TProfileSetupFormValues = { first_name: string; @@ -31,6 +33,7 @@ type TProfileSetupFormValues = { avatar?: string | null; password?: string; confirm_password?: string; + role?: string; use_case?: string; }; @@ -40,6 +43,7 @@ const defaultValues: Partial = { avatar: "", password: undefined, confirm_password: undefined, + role: undefined, use_case: undefined, }; @@ -50,15 +54,25 @@ type Props = { finishOnboarding: () => Promise; }; -const USE_CASES = [ - "Build Products", - "Manage Feedbacks", - "Service delivery", - "Field force management", - "Code Repository Integration", - "Bug Tracking", - "Test Case Management", - "Resource allocation", +enum EProfileSetupSteps { + ALL = "ALL", + USER_DETAILS = "USER_DETAILS", + USER_PERSONALIZATION = "USER_PERSONALIZATION", +} + +const USER_ROLE = ["Individual contributor", "Senior Leader", "Manager", "Executive", "Freelancer", "Student"]; + +const USER_DOMAIN = [ + "Engineering", + "Product", + "Marketing", + "Sales", + "Operations", + "Legal", + "Finance", + "Human Resources", + "Project", + "Other", ]; const fileService = new FileService(); @@ -67,6 +81,9 @@ const authService = new AuthService(); export const ProfileSetup: React.FC = observer((props) => { const { user, totalSteps, stepChange, finishOnboarding } = props; // states + const [profileSetupStep, setProfileSetupStep] = useState( + user?.is_password_autoset ? EProfileSetupSteps.USER_DETAILS : EProfileSetupSteps.ALL + ); const [isRemoving, setIsRemoving] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); @@ -95,37 +112,25 @@ export const ProfileSetup: React.FC = observer((props) => { mode: "onChange", }); - const handleUserDetailUpdate = async (data: Partial) => { - await updateCurrentUser(data); - }; - - const handleUserProfileUpdate = async (data: Partial) => { - await updateUserProfile(data); - }; - const handleSetPassword = async (password: string) => { const token = await authService.requestCSRFToken().then((data) => data?.csrf_token); await authService.setPassword(token, { password }); }; - const onSubmit = async (formData: TProfileSetupFormValues) => { - if (!user) return; - + const handleSubmitProfileSetup = async (formData: TProfileSetupFormValues) => { const userDetailsPayload: Partial = { first_name: formData.first_name, last_name: formData.last_name, avatar: formData.avatar, }; - const profileUpdatePayload: Partial = { use_case: formData.use_case, + role: formData.role, }; - try { await Promise.all([ - handleUserDetailUpdate(userDetailsPayload), - handleUserProfileUpdate(profileUpdatePayload), - formData.password ? handleSetPassword(formData.password) : Promise.resolve(), + updateCurrentUser(userDetailsPayload), + updateUserProfile(profileUpdatePayload), stepChange({ profile_complete: true }), ]).then(() => { captureEvent(USER_DETAILS, { @@ -137,7 +142,8 @@ export const ProfileSetup: React.FC = observer((props) => { title: "Success", message: "Profile setup completed!", }); - if (totalSteps === 1) { + // For Invited Users, they will skip all other steps and finish onboarding. + if (totalSteps <= 2) { finishOnboarding(); } }); @@ -154,6 +160,71 @@ export const ProfileSetup: React.FC = observer((props) => { } }; + const handleSubmitUserDetail = async (formData: TProfileSetupFormValues) => { + const userDetailsPayload: Partial = { + first_name: formData.first_name, + last_name: formData.last_name, + avatar: formData.avatar, + }; + try { + await Promise.all([ + updateCurrentUser(userDetailsPayload), + formData.password ? handleSetPassword(formData.password) : Promise.resolve(), + ]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION)); + } catch { + captureEvent(USER_DETAILS, { + state: "FAILED", + element: "Onboarding", + }); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error", + message: "User details update failed. Please try again!", + }); + } + }; + + const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => { + const profileUpdatePayload: Partial = { + use_case: formData.use_case, + role: formData.role, + }; + try { + await Promise.all([updateUserProfile(profileUpdatePayload), stepChange({ profile_complete: true })]).then(() => { + captureEvent(USER_DETAILS, { + state: "SUCCESS", + element: "Onboarding", + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success", + message: "Profile setup completed!", + }); + // For Invited Users, they will skip all other steps and finish onboarding. + if (totalSteps <= 2) { + finishOnboarding(); + } + }); + } catch { + captureEvent(USER_DETAILS, { + state: "FAILED", + element: "Onboarding", + }); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error", + message: "Profile setup failed. Please try again!", + }); + } + }; + + const onSubmit = async (formData: TProfileSetupFormValues) => { + if (!user) return; + if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData); + if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData); + if (profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION) await handleSubmitUserPersonalization(formData); + }; + const handleDelete = (url: string | null | undefined) => { if (!url) return; @@ -172,6 +243,8 @@ export const ProfileSetup: React.FC = observer((props) => { const isValidPassword = (password: string, confirmPassword?: string) => getPasswordStrength(password) >= 3 && password === confirmPassword; + // Check for all available fields validation and if password field is available, then checks for password validation (strength + confirmation). + // Also handles the condition for optional password i.e if password field is optional it only checks for above validation if it's not empty. const isButtonDisabled = useMemo( () => isValid && @@ -187,242 +260,286 @@ export const ProfileSetup: React.FC = observer((props) => { [isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword] ); + const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION; + return (
- +
-

Welcome to Plane!

+

+ {isCurrentStepUserPersonalization + ? `Looking good${user?.first_name && `, ${user.first_name}`}!` + : "Welcome to Plane!"} +

- Let’s setup your profile, tell us a bit about yourself. + {isCurrentStepUserPersonalization + ? "Let’s personalize Plane for you." + : "Let’s setup your profile, tell us a bit about yourself."}

- ( - setIsImageUploadModalOpen(false)} - isRemoving={isRemoving} - handleDelete={() => handleDelete(getValues("avatar"))} - onSuccess={(url) => { - onChange(url); - setIsImageUploadModalOpen(false); - }} - value={value && value.trim() !== "" ? value : null} - /> - )} - /> -
- -
-
-
- + {profileSetupStep !== EProfileSetupSteps.USER_PERSONALIZATION && ( + <> ( - ( + setIsImageUploadModalOpen(false)} + isRemoving={isRemoving} + handleDelete={() => handleDelete(getValues("avatar"))} + onSuccess={(url) => { + onChange(url); + setIsImageUploadModalOpen(false); + }} + value={value && value.trim() !== "" ? value : null} + /> + )} + /> +
+ +
+
+
+ + ( + + )} /> - )} - /> - {errors.first_name && {errors.first_name.message}} -
-
- - ( - {errors.first_name.message}} +
+
+ + ( + + )} /> - )} - /> - {errors.last_name && {errors.last_name.message}} -
-
- {!isPasswordAlreadySetup && ( -
- - ( -
- setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - /> - {showPassword ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> + {errors.last_name && {errors.last_name.message}} +
+
+ {!isPasswordAlreadySetup && ( +
+ + ( +
+ setIsPasswordInputFocused(true)} + onBlur={() => setIsPasswordInputFocused(false)} + /> + {showPassword ? ( + setShowPassword(false)} + /> + ) : ( + setShowPassword(true)} + /> + )} +
)} -
- )} - /> - {isPasswordInputFocused && } - {errors.password && {errors.password.message}} -
- )} - {!isPasswordAlreadySetup && password && getPasswordStrength(password) >= 3 && ( -
- - value === password || "Password doesn't match", - }} - render={({ field: { value, onChange, ref } }) => ( -
- - {showPassword ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> - )} -
- )} - /> - {errors.confirm_password && ( - {errors.confirm_password.message} - )} -
- )} -
- - ( -
- {USE_CASES.map((useCase) => ( -
onChange(useCase)} - > - {useCase} -
- ))} + /> + {isPasswordInputFocused && } + {errors.password && {errors.password.message}}
)} - /> - {errors.use_case && {errors.use_case.message}} -
+ {!isPasswordAlreadySetup && password && getPasswordStrength(password) >= 3 && ( +
+ + value === password || "Password doesn't match", + }} + render={({ field: { value, onChange, ref } }) => ( +
+ + {showPassword ? ( + setShowPassword(false)} + /> + ) : ( + setShowPassword(true)} + /> + )} +
+ )} + /> + {errors.confirm_password && ( + {errors.confirm_password.message} + )} +
+ )} + + )} + {profileSetupStep !== EProfileSetupSteps.USER_DETAILS && ( + <> +
+ + ( +
+ {USER_ROLE.map((userRole) => ( +
onChange(userRole)} + > + {userRole} +
+ ))} +
+ )} + /> + {errors.role && {errors.role.message}} +
+
+ + ( +
+ {USER_DOMAIN.map((userDomain) => ( +
onChange(userDomain)} + > + {userDomain} +
+ ))} +
+ )} + /> + {errors.use_case && {errors.use_case.message}} +
+ + )}
diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 327fbca3a..ef7fa29a3 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -91,8 +91,16 @@ const OnboardingPage: NextPageWithLayout = observer(() => { }; useEffect(() => { - if (workspacesList && workspacesList?.length > 0) setTotalSteps(1); - else setTotalSteps(3); + // If user is already invited to a workspace, only show profile setup steps. + if (workspacesList && workspacesList?.length > 0) { + // If password is auto set then show two different steps for profile setup, else merge them. + if (user?.is_password_autoset) setTotalSteps(2); + else setTotalSteps(1); + } else { + // If password is auto set then total steps will increase to 4 due to extra step at profile setup stage. + if (user?.is_password_autoset) setTotalSteps(4); + else setTotalSteps(3); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -122,7 +130,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { } // For Invited Users, they will skip all other steps. - if (totalSteps && totalSteps === 1) return; + if (totalSteps && totalSteps <= 2) return; if (onboardingStep.profile_complete && !(onboardingStep.workspace_join || onboardingStep.workspace_create)) { setStep(EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN); diff --git a/web/public/onboarding/user-personalization-dark.svg b/web/public/onboarding/user-personalization-dark.svg new file mode 100644 index 000000000..a761257d0 --- /dev/null +++ b/web/public/onboarding/user-personalization-dark.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/onboarding/user-personalization-light.svg b/web/public/onboarding/user-personalization-light.svg new file mode 100644 index 000000000..80176a09e --- /dev/null +++ b/web/public/onboarding/user-personalization-light.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +