From 7ad0466d6505816b69dc69bd66e53dd4f77fd5b3 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:23:49 +0530 Subject: [PATCH] refactor: new onboarding workflow (#1724) * refactor: new onboarding workflow * refactor: new onboarding workflow --- .../components/onboarding/invite-members.tsx | 49 +++------- .../components/onboarding/join-workspaces.tsx | 47 +++++----- apps/app/components/onboarding/workspace.tsx | 37 ++++++-- apps/app/pages/onboarding.tsx | 90 +++++++++++++------ apps/app/types/users.d.ts | 4 +- 5 files changed, 125 insertions(+), 102 deletions(-) diff --git a/apps/app/components/onboarding/invite-members.tsx b/apps/app/components/onboarding/invite-members.tsx index 9bd2a82e8..fee1cb252 100644 --- a/apps/app/components/onboarding/invite-members.tsx +++ b/apps/app/components/onboarding/invite-members.tsx @@ -1,12 +1,9 @@ import React, { useEffect } from "react"; -import useSWR, { mutate } from "swr"; - // react-hook-form import { Controller, useFieldArray, useForm } from "react-hook-form"; // services import workspaceService from "services/workspace.service"; -import userService from "services/user.service"; // hooks import useToast from "hooks/use-toast"; // ui @@ -14,16 +11,15 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ // icons import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; // types -import { ICurrentUserResponse, IWorkspace, OnboardingSteps } from "types"; -// fetch-keys -import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; +import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types"; // constants import { ROLE } from "constants/workspace"; type Props = { - workspace: IWorkspace | undefined; + finishOnboarding: () => Promise; + stepChange: (steps: Partial) => Promise; user: ICurrentUserResponse | undefined; - stepChange: (steps: Partial) => Promise; + workspace: IWorkspace | undefined; }; type EmailRole = { @@ -35,7 +31,12 @@ type FormValues = { emails: EmailRole[]; }; -export const InviteMembers: React.FC = ({ workspace, user, stepChange }) => { +export const InviteMembers: React.FC = ({ + finishOnboarding, + stepChange, + user, + workspace, +}) => { const { setToastAlert } = useToast(); const { @@ -49,38 +50,14 @@ export const InviteMembers: React.FC = ({ workspace, user, stepChange }) name: "emails", }); - const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => - workspaceService.userWorkspaceInvitations() - ); - const nextStep = async () => { - if (!user || !invitations) return; - - const payload: Partial = { + const payload: Partial = { workspace_invite: true, + workspace_join: true, }; - // update onboarding status from this step if no invitations are present - if (invitations.length === 0) { - payload.workspace_join = true; - - mutate( - CURRENT_USER, - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - is_onboarded: true, - }; - }, - false - ); - - await userService.updateUserOnBoard({ userRole: user.role }, user); - } - await stepChange(payload); + await finishOnboarding(); }; const onSubmit = async (formData: FormValues) => { diff --git a/apps/app/components/onboarding/join-workspaces.tsx b/apps/app/components/onboarding/join-workspaces.tsx index 2441542d6..84b5bdfc9 100644 --- a/apps/app/components/onboarding/join-workspaces.tsx +++ b/apps/app/components/onboarding/join-workspaces.tsx @@ -4,7 +4,6 @@ import useSWR, { mutate } from "swr"; // services import workspaceService from "services/workspace.service"; -import userService from "services/user.service"; // hooks import useUser from "hooks/use-user"; // ui @@ -14,17 +13,23 @@ import { CheckCircleIcon } from "@heroicons/react/24/outline"; // helpers import { truncateText } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IUser, IWorkspaceMemberInvitation, OnboardingSteps } from "types"; +import { IWorkspaceMemberInvitation, TOnboardingSteps } from "types"; // fetch-keys -import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; +import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // constants import { ROLE } from "constants/workspace"; type Props = { - stepChange: (steps: Partial) => Promise; + finishOnboarding: () => Promise; + stepChange: (steps: Partial) => Promise; + updateLastWorkspace: () => Promise; }; -export const JoinWorkspaces: React.FC = ({ stepChange }) => { +export const JoinWorkspaces: React.FC = ({ + finishOnboarding, + stepChange, + updateLastWorkspace, +}) => { const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [invitationsRespond, setInvitationsRespond] = useState([]); @@ -47,25 +52,13 @@ export const JoinWorkspaces: React.FC = ({ stepChange }) => { } }; - // complete onboarding - const finishOnboarding = async () => { + const handleNextStep = async () => { if (!user) return; - mutate( - CURRENT_USER, - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - is_onboarded: true, - }; - }, - false - ); - - await userService.updateUserOnBoard({ userRole: user.role }, user); await stepChange({ workspace_join: true }); + + if (user.onboarding_step.workspace_create && user.onboarding_step.workspace_invite) + await finishOnboarding(); }; const submitInvitations = async () => { @@ -77,11 +70,12 @@ export const JoinWorkspaces: React.FC = ({ stepChange }) => { .joinWorkspaces({ invitations: invitationsRespond }) .then(async () => { await mutateInvitations(); - await finishOnboarding(); + await mutate(USER_WORKSPACES); + await updateLastWorkspace(); - setIsJoiningWorkspaces(false); + await handleNextStep(); }) - .catch(() => setIsJoiningWorkspaces(false)); + .finally(() => setIsJoiningWorkspaces(false)); }; return ( @@ -142,14 +136,15 @@ export const JoinWorkspaces: React.FC = ({ stepChange }) => { type="submit" size="md" onClick={submitInvitations} - disabled={isJoiningWorkspaces || invitationsRespond.length === 0} + disabled={invitationsRespond.length === 0} + loading={isJoiningWorkspaces} > Accept & Join Skip for now diff --git a/apps/app/components/onboarding/workspace.tsx b/apps/app/components/onboarding/workspace.tsx index b401585dd..8f7442432 100644 --- a/apps/app/components/onboarding/workspace.tsx +++ b/apps/app/components/onboarding/workspace.tsx @@ -3,17 +3,25 @@ import { useState } from "react"; // ui import { SecondaryButton } from "components/ui"; // types -import { ICurrentUserResponse, OnboardingSteps } from "types"; +import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types"; // constants import { CreateWorkspaceForm } from "components/workspace"; type Props = { - user: ICurrentUserResponse | undefined; + finishOnboarding: () => Promise; + stepChange: (steps: Partial) => Promise; updateLastWorkspace: () => Promise; - stepChange: (steps: Partial) => Promise; + user: ICurrentUserResponse | undefined; + workspaces: IWorkspace[] | undefined; }; -export const Workspace: React.FC = ({ user, updateLastWorkspace, stepChange }) => { +export const Workspace: React.FC = ({ + finishOnboarding, + stepChange, + updateLastWorkspace, + user, + workspaces, +}) => { const [defaultValues, setDefaultValues] = useState({ name: "", slug: "", @@ -23,12 +31,21 @@ export const Workspace: React.FC = ({ user, updateLastWorkspace, stepChan const completeStep = async () => { if (!user) return; - await stepChange({ + const payload: Partial = { workspace_create: true, - }); + }; + + await stepChange(payload); await updateLastWorkspace(); }; + const secondaryButtonAction = async () => { + if (workspaces && workspaces.length > 0) { + await stepChange({ workspace_create: true, workspace_invite: true, workspace_join: true }); + await finishOnboarding(); + } else await stepChange({ profile_complete: false, workspace_join: false }); + }; + return (

Create your workspace

@@ -43,9 +60,11 @@ export const Workspace: React.FC = ({ user, updateLastWorkspace, stepChan default: "Continue", }} secondaryButton={ - stepChange({ profile_complete: false })}> - Back - + workspaces ? ( + + {workspaces.length > 0 ? "Skip & continue" : "Back"} + + ) : undefined } />
diff --git a/apps/app/pages/onboarding.tsx b/apps/app/pages/onboarding.tsx index 57b121648..217584dd0 100644 --- a/apps/app/pages/onboarding.tsx +++ b/apps/app/pages/onboarding.tsx @@ -24,7 +24,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; // types -import { ICurrentUserResponse, IUser, OnboardingSteps } from "types"; +import { ICurrentUserResponse, IUser, TOnboardingSteps } from "types"; import type { NextPage } from "next"; // fetch-keys import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; @@ -43,33 +43,35 @@ const Onboarding: NextPage = () => { workspaceService.userWorkspaceInvitations() ); + // update last active workspace details const updateLastWorkspace = async () => { - if (!userWorkspaces) return; + if (!workspaces) return; - mutate( + await mutate( CURRENT_USER, (prevData) => { if (!prevData) return prevData; return { ...prevData, - last_workspace_id: userWorkspaces[0]?.id, + last_workspace_id: workspaces[0]?.id, workspace: { ...prevData.workspace, - fallback_workspace_id: userWorkspaces[0]?.id, - fallback_workspace_slug: userWorkspaces[0]?.slug, - last_workspace_id: userWorkspaces[0]?.id, - last_workspace_slug: userWorkspaces[0]?.slug, + fallback_workspace_id: workspaces[0]?.id, + fallback_workspace_slug: workspaces[0]?.slug, + last_workspace_id: workspaces[0]?.id, + last_workspace_slug: workspaces[0]?.slug, }, }; }, false ); - await userService.updateUser({ last_workspace_id: userWorkspaces?.[0]?.id }); + await userService.updateUser({ last_workspace_id: workspaces?.[0]?.id }); }; - const stepChange = async (steps: Partial) => { + // handle step change + const stepChange = async (steps: Partial) => { if (!user) return; const payload: Partial = { @@ -95,16 +97,44 @@ const Onboarding: NextPage = () => { await userService.updateUser(payload); }; + // complete onboarding + const finishOnboarding = async () => { + if (!user) return; + + mutate( + CURRENT_USER, + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + is_onboarded: true, + }; + }, + false + ); + + await userService.updateUserOnBoard({ userRole: user.role }, user); + }; + useEffect(() => { const handleStepChange = async () => { - if (!user || !userWorkspaces || !invitations) return; + if (!user || !invitations) return; const onboardingStep = user.onboarding_step; if (!onboardingStep.profile_complete && step !== 1) setStep(1); - if (onboardingStep.profile_complete && !onboardingStep.workspace_create && step !== 2) - setStep(2); + if (onboardingStep.profile_complete) { + if (!onboardingStep.workspace_join && invitations.length > 0 && step !== 2 && step !== 4) + setStep(4); + else if ( + !onboardingStep.workspace_create && + (step !== 4 || onboardingStep.workspace_join) && + step !== 2 + ) + setStep(2); + } if ( onboardingStep.profile_complete && @@ -113,21 +143,10 @@ const Onboarding: NextPage = () => { step !== 3 ) setStep(3); - - if ( - onboardingStep.profile_complete && - onboardingStep.workspace_create && - onboardingStep.workspace_invite && - !onboardingStep.workspace_join && - step !== 4 - ) { - if (invitations.length > 0) setStep(4); - else await Router.push("/"); - } }; handleStepChange(); - }, [user, invitations, userWorkspaces, step]); + }, [user, invitations, step]); if (userLoading || step === null) return ( @@ -167,14 +186,27 @@ const Onboarding: NextPage = () => { ) : step === 2 ? ( ) : step === 3 ? ( - + ) : ( - step === 4 && + step === 4 && ( + + ) )} {step !== 4 && ( diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index 3d72f3300..8731d29ad 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -27,7 +27,7 @@ export interface IUser { properties: Properties; groupBy: NestedKeyOf | null; } | null; - onboarding_step: OnboardingSteps; + onboarding_step: TOnboardingSteps; role: string; token: string; theme: ICustomTheme; @@ -140,7 +140,7 @@ export type UserAuth = { isGuest: boolean; }; -export type OnboardingSteps = { +export type TOnboardingSteps = { profile_complete: boolean; workspace_create: boolean; workspace_invite: boolean;