refactor: new onboarding workflow (#1724)

* refactor: new onboarding workflow

* refactor: new onboarding workflow
This commit is contained in:
Aaryan Khandelwal 2023-07-31 17:23:49 +05:30 committed by GitHub
parent e8f748a67d
commit 7ad0466d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 102 deletions

View File

@ -1,12 +1,9 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import useSWR, { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
@ -14,16 +11,15 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/
// icons // icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IWorkspace, OnboardingSteps } from "types"; import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
type Props = { type Props = {
workspace: IWorkspace | undefined; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; workspace: IWorkspace | undefined;
}; };
type EmailRole = { type EmailRole = {
@ -35,7 +31,12 @@ type FormValues = {
emails: EmailRole[]; emails: EmailRole[];
}; };
export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange }) => { export const InviteMembers: React.FC<Props> = ({
finishOnboarding,
stepChange,
user,
workspace,
}) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { const {
@ -49,38 +50,14 @@ export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange })
name: "emails", name: "emails",
}); });
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const nextStep = async () => { const nextStep = async () => {
if (!user || !invitations) return; const payload: Partial<TOnboardingSteps> = {
const payload: Partial<OnboardingSteps> = {
workspace_invite: true, 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<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
}
await stepChange(payload); await stepChange(payload);
await finishOnboarding();
}; };
const onSubmit = async (formData: FormValues) => { const onSubmit = async (formData: FormValues) => {

View File

@ -4,7 +4,6 @@ import useSWR, { mutate } from "swr";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// ui // ui
@ -14,17 +13,23 @@ import { CheckCircleIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IUser, IWorkspaceMemberInvitation, OnboardingSteps } from "types"; import { IWorkspaceMemberInvitation, TOnboardingSteps } from "types";
// fetch-keys // fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
type Props = { type Props = {
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
updateLastWorkspace: () => Promise<void>;
}; };
export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => { export const JoinWorkspaces: React.FC<Props> = ({
finishOnboarding,
stepChange,
updateLastWorkspace,
}) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
@ -47,25 +52,13 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
} }
}; };
// complete onboarding const handleNextStep = async () => {
const finishOnboarding = async () => {
if (!user) return; if (!user) return;
mutate<ICurrentUserResponse>(
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 }); await stepChange({ workspace_join: true });
if (user.onboarding_step.workspace_create && user.onboarding_step.workspace_invite)
await finishOnboarding();
}; };
const submitInvitations = async () => { const submitInvitations = async () => {
@ -77,11 +70,12 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
.joinWorkspaces({ invitations: invitationsRespond }) .joinWorkspaces({ invitations: invitationsRespond })
.then(async () => { .then(async () => {
await mutateInvitations(); await mutateInvitations();
await finishOnboarding(); await mutate(USER_WORKSPACES);
await updateLastWorkspace();
setIsJoiningWorkspaces(false); await handleNextStep();
}) })
.catch(() => setIsJoiningWorkspaces(false)); .finally(() => setIsJoiningWorkspaces(false));
}; };
return ( return (
@ -142,14 +136,15 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
type="submit" type="submit"
size="md" size="md"
onClick={submitInvitations} onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0} disabled={invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
> >
Accept & Join Accept & Join
</PrimaryButton> </PrimaryButton>
<SecondaryButton <SecondaryButton
className="border border-none bg-transparent" className="border border-none bg-transparent"
size="md" size="md"
onClick={finishOnboarding} onClick={handleNextStep}
> >
Skip for now Skip for now
</SecondaryButton> </SecondaryButton>

View File

@ -3,17 +3,25 @@ import { useState } from "react";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
// types // types
import { ICurrentUserResponse, OnboardingSteps } from "types"; import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
// constants // constants
import { CreateWorkspaceForm } from "components/workspace"; import { CreateWorkspaceForm } from "components/workspace";
type Props = { type Props = {
user: ICurrentUserResponse | undefined; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
updateLastWorkspace: () => Promise<void>; updateLastWorkspace: () => Promise<void>;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; user: ICurrentUserResponse | undefined;
workspaces: IWorkspace[] | undefined;
}; };
export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChange }) => { export const Workspace: React.FC<Props> = ({
finishOnboarding,
stepChange,
updateLastWorkspace,
user,
workspaces,
}) => {
const [defaultValues, setDefaultValues] = useState({ const [defaultValues, setDefaultValues] = useState({
name: "", name: "",
slug: "", slug: "",
@ -23,12 +31,21 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
const completeStep = async () => { const completeStep = async () => {
if (!user) return; if (!user) return;
await stepChange({ const payload: Partial<TOnboardingSteps> = {
workspace_create: true, workspace_create: true,
}); };
await stepChange(payload);
await updateLastWorkspace(); 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 ( return (
<div className="w-full space-y-7 sm:space-y-10"> <div className="w-full space-y-7 sm:space-y-10">
<h4 className="text-xl sm:text-2xl font-semibold">Create your workspace</h4> <h4 className="text-xl sm:text-2xl font-semibold">Create your workspace</h4>
@ -43,9 +60,11 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
default: "Continue", default: "Continue",
}} }}
secondaryButton={ secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}> workspaces ? (
Back <SecondaryButton onClick={secondaryButtonAction}>
{workspaces.length > 0 ? "Skip & continue" : "Back"}
</SecondaryButton> </SecondaryButton>
) : undefined
} }
/> />
</div> </div>

View File

@ -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 BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types // types
import { ICurrentUserResponse, IUser, OnboardingSteps } from "types"; import { ICurrentUserResponse, IUser, TOnboardingSteps } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
@ -43,33 +43,35 @@ const Onboarding: NextPage = () => {
workspaceService.userWorkspaceInvitations() workspaceService.userWorkspaceInvitations()
); );
// update last active workspace details
const updateLastWorkspace = async () => { const updateLastWorkspace = async () => {
if (!userWorkspaces) return; if (!workspaces) return;
mutate<ICurrentUserResponse>( await mutate<ICurrentUserResponse>(
CURRENT_USER, CURRENT_USER,
(prevData) => { (prevData) => {
if (!prevData) return prevData; if (!prevData) return prevData;
return { return {
...prevData, ...prevData,
last_workspace_id: userWorkspaces[0]?.id, last_workspace_id: workspaces[0]?.id,
workspace: { workspace: {
...prevData.workspace, ...prevData.workspace,
fallback_workspace_id: userWorkspaces[0]?.id, fallback_workspace_id: workspaces[0]?.id,
fallback_workspace_slug: userWorkspaces[0]?.slug, fallback_workspace_slug: workspaces[0]?.slug,
last_workspace_id: userWorkspaces[0]?.id, last_workspace_id: workspaces[0]?.id,
last_workspace_slug: userWorkspaces[0]?.slug, last_workspace_slug: workspaces[0]?.slug,
}, },
}; };
}, },
false false
); );
await userService.updateUser({ last_workspace_id: userWorkspaces?.[0]?.id }); await userService.updateUser({ last_workspace_id: workspaces?.[0]?.id });
}; };
const stepChange = async (steps: Partial<OnboardingSteps>) => { // handle step change
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
if (!user) return; if (!user) return;
const payload: Partial<IUser> = { const payload: Partial<IUser> = {
@ -95,16 +97,44 @@ const Onboarding: NextPage = () => {
await userService.updateUser(payload); await userService.updateUser(payload);
}; };
// complete onboarding
const finishOnboarding = async () => {
if (!user) return;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
};
useEffect(() => { useEffect(() => {
const handleStepChange = async () => { const handleStepChange = async () => {
if (!user || !userWorkspaces || !invitations) return; if (!user || !invitations) return;
const onboardingStep = user.onboarding_step; const onboardingStep = user.onboarding_step;
if (!onboardingStep.profile_complete && step !== 1) setStep(1); if (!onboardingStep.profile_complete && step !== 1) setStep(1);
if (onboardingStep.profile_complete && !onboardingStep.workspace_create && step !== 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); setStep(2);
}
if ( if (
onboardingStep.profile_complete && onboardingStep.profile_complete &&
@ -113,21 +143,10 @@ const Onboarding: NextPage = () => {
step !== 3 step !== 3
) )
setStep(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(); handleStepChange();
}, [user, invitations, userWorkspaces, step]); }, [user, invitations, step]);
if (userLoading || step === null) if (userLoading || step === null)
return ( return (
@ -167,14 +186,27 @@ const Onboarding: NextPage = () => {
<UserDetails user={user} /> <UserDetails user={user} />
) : step === 2 ? ( ) : step === 2 ? (
<Workspace <Workspace
user={user} finishOnboarding={finishOnboarding}
updateLastWorkspace={updateLastWorkspace}
stepChange={stepChange} stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/> />
) : step === 3 ? ( ) : step === 3 ? (
<InviteMembers workspace={userWorkspaces?.[0]} user={user} stepChange={stepChange} /> <InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={userWorkspaces?.[0]}
/>
) : ( ) : (
step === 4 && <JoinWorkspaces stepChange={stepChange} /> step === 4 && (
<JoinWorkspaces
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
/>
)
)} )}
</div> </div>
{step !== 4 && ( {step !== 4 && (

View File

@ -27,7 +27,7 @@ export interface IUser {
properties: Properties; properties: Properties;
groupBy: NestedKeyOf<IIssue> | null; groupBy: NestedKeyOf<IIssue> | null;
} | null; } | null;
onboarding_step: OnboardingSteps; onboarding_step: TOnboardingSteps;
role: string; role: string;
token: string; token: string;
theme: ICustomTheme; theme: ICustomTheme;
@ -140,7 +140,7 @@ export type UserAuth = {
isGuest: boolean; isGuest: boolean;
}; };
export type OnboardingSteps = { export type TOnboardingSteps = {
profile_complete: boolean; profile_complete: boolean;
workspace_create: boolean; workspace_create: boolean;
workspace_invite: boolean; workspace_invite: boolean;