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 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<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
user: ICurrentUserResponse | undefined;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>;
workspace: IWorkspace | undefined;
};
type EmailRole = {
@ -35,7 +31,12 @@ type FormValues = {
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 {
@ -49,38 +50,14 @@ export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange })
name: "emails",
});
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const nextStep = async () => {
if (!user || !invitations) return;
const payload: Partial<OnboardingSteps> = {
const payload: Partial<TOnboardingSteps> = {
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 finishOnboarding();
};
const onSubmit = async (formData: FormValues) => {

View File

@ -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<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 [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
@ -47,25 +52,13 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
}
};
// complete onboarding
const finishOnboarding = async () => {
const handleNextStep = 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);
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<Props> = ({ 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<Props> = ({ stepChange }) => {
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
disabled={invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
>
Accept & Join
</PrimaryButton>
<SecondaryButton
className="border border-none bg-transparent"
size="md"
onClick={finishOnboarding}
onClick={handleNextStep}
>
Skip for now
</SecondaryButton>

View File

@ -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<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => 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({
name: "",
slug: "",
@ -23,12 +31,21 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
const completeStep = async () => {
if (!user) return;
await stepChange({
const payload: Partial<TOnboardingSteps> = {
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 (
<div className="w-full space-y-7 sm:space-y-10">
<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",
}}
secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}>
Back
</SecondaryButton>
workspaces ? (
<SecondaryButton onClick={secondaryButtonAction}>
{workspaces.length > 0 ? "Skip & continue" : "Back"}
</SecondaryButton>
) : undefined
}
/>
</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 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<ICurrentUserResponse>(
await mutate<ICurrentUserResponse>(
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<OnboardingSteps>) => {
// handle step change
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
if (!user) return;
const payload: Partial<IUser> = {
@ -95,16 +97,44 @@ const Onboarding: NextPage = () => {
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(() => {
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 = () => {
<UserDetails user={user} />
) : step === 2 ? (
<Workspace
user={user}
updateLastWorkspace={updateLastWorkspace}
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/>
) : 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>
{step !== 4 && (

View File

@ -27,7 +27,7 @@ export interface IUser {
properties: Properties;
groupBy: NestedKeyOf<IIssue> | 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;