forked from github/plane
refactor: new onboarding workflow (#1724)
* refactor: new onboarding workflow * refactor: new onboarding workflow
This commit is contained in:
parent
e8f748a67d
commit
7ad0466d65
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
workspaces ? (
|
||||
<SecondaryButton onClick={secondaryButtonAction}>
|
||||
{workspaces.length > 0 ? "Skip & continue" : "Back"}
|
||||
</SecondaryButton>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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)
|
||||
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 && (
|
||||
|
4
apps/app/types/users.d.ts
vendored
4
apps/app/types/users.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user