chore: onboarding ui updates and accept invitation workflow updates.

This commit is contained in:
Prateek Shourya 2024-05-01 12:06:51 +05:30
parent 43ce850ae9
commit 4330f0f0c9
13 changed files with 251 additions and 254 deletions

View File

@ -58,7 +58,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]); }, [csrfToken]);
const redirectToUniqueCodeLogin = async () => { const redirectToUniqueCodeSignIn = async () => {
handleStepChange(EAuthSteps.UNIQUE_CODE); handleStepChange(EAuthSteps.UNIQUE_CODE);
}; };
@ -194,7 +194,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{instance && isSmtpConfigured && ( {instance && isSmtpConfigured && (
<Button <Button
type="button" type="button"
onClick={redirectToUniqueCodeLogin} onClick={redirectToUniqueCodeSignIn}
variant="outline-primary" variant="outline-primary"
className="w-full" className="w-full"
size="lg" size="lg"

View File

@ -39,19 +39,19 @@ type Props = {
mode: EAuthModes; mode: EAuthModes;
}; };
const Titles = { const titles = {
[EAuthModes.SIGN_IN]: { [EAuthModes.SIGN_IN]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "Sign in to Plane", header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress", subHeader: "Get back to your projects and make progress.",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "Sign in to Plane", header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress", subHeader: "Get back to your projects and make progress.",
}, },
[EAuthSteps.UNIQUE_CODE]: { [EAuthSteps.UNIQUE_CODE]: {
header: "Sign in to Plane", header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress", subHeader: "Get back to your projects and make progress.",
}, },
[EAuthSteps.OPTIONAL_SET_PASSWORD]: { [EAuthSteps.OPTIONAL_SET_PASSWORD]: {
header: "", header: "",
@ -61,7 +61,7 @@ const Titles = {
[EAuthModes.SIGN_UP]: { [EAuthModes.SIGN_UP]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "Create your account", header: "Create your account",
subHeader: "Start tracking your projects with Plane", subHeader: "Start tracking your projects with Plane.",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "Create your account", header: "Create your account",
@ -98,7 +98,7 @@ const getHeaderSubHeader = (
}; };
} }
return Titles[mode][step]; return titles[mode][step];
}; };
export const AuthRoot = observer((props: Props) => { export const AuthRoot = observer((props: Props) => {

View File

@ -72,10 +72,10 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
); );
}; };
const handleRequestNewCode = async () => { const handleRequestNewCode = async (email: string) => {
setIsRequestingNewCode(true); setIsRequestingNewCode(true);
await handleSendNewCode(uniqueCodeFormData.email) await handleSendNewCode(email)
.then(() => setResendCodeTimer(30)) .then(() => setResendCodeTimer(30))
.finally(() => setIsRequestingNewCode(false)); .finally(() => setIsRequestingNewCode(false));
}; };
@ -86,10 +86,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
}, [csrfToken]); }, [csrfToken]);
useEffect(() => { useEffect(() => {
setIsRequestingNewCode(true); handleRequestNewCode(email);
handleSendNewCode(email)
.then(() => setResendCodeTimer(30))
.finally(() => setIsRequestingNewCode(false));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@ -149,7 +146,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
</p> </p>
<button <button
type="button" type="button"
onClick={handleRequestNewCode} onClick={() => handleRequestNewCode(uniqueCodeFormData.email)}
className={`${ className={`${
isRequestNewCodeDisabled isRequestNewCodeDisabled
? "text-onboarding-text-400" ? "text-onboarding-text-400"
@ -165,7 +162,14 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
</button> </button>
</div> </div>
</div> </div>
<Button type="submit" variant="primary" className="w-full" size="lg" loading={isRequestingNewCode}> <Button
type="submit"
variant="primary"
className="w-full"
size="lg"
loading={isRequestingNewCode}
disabled={isRequestingNewCode || !uniqueCodeFormData.code}
>
{isRequestingNewCode ? "Sending code" : submitButtonText} {isRequestingNewCode ? "Sending code" : submitButtonText}
</Button> </Button>
</form> </form>

View File

@ -24,10 +24,11 @@ type Props = {
invitations: IWorkspaceMemberInvitation[]; invitations: IWorkspaceMemberInvitation[];
totalSteps: number; totalSteps: number;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>; stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
finishOnboarding: () => Promise<void>;
}; };
export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => { export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
const { invitations, totalSteps, stepChange } = props; const { invitations, totalSteps, stepChange, finishOnboarding } = props;
// states // states
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null); const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
// store hooks // store hooks
@ -45,14 +46,15 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
const handleNextStep = async () => { const handleNextStep = async () => {
if (!user) return; if (!user) return;
await stepChange({ workspace_join: true, workspace_create: true });
await finishOnboarding();
}; };
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<OnboardingHeader currentStep={2} totalSteps={totalSteps} /> <OnboardingHeader currentStep={totalSteps - 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden"> <div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown /> <SwitchOrDeleteAccountDropdown />
</div> </div>

View File

@ -1,25 +1,23 @@
import React, { useState } from "react"; import React, { useState } from "react";
import useSWR, { mutate } from "swr"; import useSWR from "swr";;
// icons
import { CheckCircle2 } from "lucide-react";
// types // types
import { IWorkspaceMemberInvitation } from "@plane/types"; import { IWorkspaceMemberInvitation } from "@plane/types";
// ui // ui
import { Button } from "@plane/ui"; import { Button, Checkbox } from "@plane/ui";
// constants // constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker"; import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys"; import { USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys";
import { ROLE } from "@/constants/workspace"; import { ROLE } from "@/constants/workspace";
// helpers // helpers
import { truncateText } from "@/helpers/string.helper"; import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper"; import { getUserRole } from "@/helpers/user.helper";
// hooks // hooks
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store"; import { useEventTracker, useWorkspace } from "@/hooks/store";
// services // services
import { WorkspaceService } from "@/services/workspace.service"; import { WorkspaceService } from "@/services/workspace.service";
type Props = { type Props = {
handleNextStep: () => void; handleNextStep: () => Promise<void>;
handleCurrentViewChange: () => void; handleCurrentViewChange: () => void;
}; };
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
@ -31,14 +29,9 @@ export const Invitations: React.FC<Props> = (props) => {
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { updateCurrentUser } = useUser(); const { fetchWorkspaces } = useWorkspace();
const { workspaces, fetchWorkspaces } = useWorkspace();
const workspacesList = Object.values(workspaces); const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => { const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
if (action === "accepted") { if (action === "accepted") {
@ -48,13 +41,6 @@ export const Invitations: React.FC<Props> = (props) => {
} }
}; };
const updateLastWorkspace = async () => {
if (!workspacesList) return;
await updateCurrentUser({
last_workspace_id: workspacesList[0]?.id,
});
};
const submitInvitations = async () => { const submitInvitations = async () => {
const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]);
@ -62,9 +48,8 @@ export const Invitations: React.FC<Props> = (props) => {
setIsJoiningWorkspaces(true); setIsJoiningWorkspaces(true);
await workspaceService try {
.joinWorkspaces({ invitations: invitationsRespond }) await workspaceService.joinWorkspaces({ invitations: invitationsRespond });
.then(async () => {
captureEvent(MEMBER_ACCEPTED, { captureEvent(MEMBER_ACCEPTED, {
member_id: invitation?.id, member_id: invitation?.id,
role: getUserRole(invitation?.role as any), role: getUserRole(invitation?.role as any),
@ -74,12 +59,8 @@ export const Invitations: React.FC<Props> = (props) => {
element: "Workspace invitations page", element: "Workspace invitations page",
}); });
await fetchWorkspaces(); await fetchWorkspaces();
await mutate(USER_WORKSPACES);
await updateLastWorkspace();
await handleNextStep(); await handleNextStep();
await mutateInvitations(); } catch (error) {
})
.catch((error) => {
console.error(error); console.error(error);
captureEvent(MEMBER_ACCEPTED, { captureEvent(MEMBER_ACCEPTED, {
member_id: invitation?.id, member_id: invitation?.id,
@ -89,15 +70,15 @@ export const Invitations: React.FC<Props> = (props) => {
state: "FAILED", state: "FAILED",
element: "Workspace invitations page", element: "Workspace invitations page",
}); });
}) setIsJoiningWorkspaces(false);
.finally(() => setIsJoiningWorkspaces(false)); }
}; };
return invitations && invitations.length > 0 ? ( return invitations && invitations.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-center space-y-1 py-4 mx-auto"> <div className="text-center space-y-1 py-4 mx-auto">
<h3 className="text-3xl font-bold text-onboarding-text-100">You are invited!</h3> <h3 className="text-3xl font-bold text-onboarding-text-100">You are invited!</h3>
<p className="font-medium text-onboarding-text-400">Accept the invites to collaborate with your team!</p> <p className="font-medium text-onboarding-text-400">Accept the invites to collaborate with your team.</p>
</div> </div>
<div> <div>
{invitations && {invitations &&
@ -108,11 +89,7 @@ export const Invitations: React.FC<Props> = (props) => {
return ( return (
<div <div
key={invitation.id} key={invitation.id}
className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 ${ className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 border-custom-border-200 hover:bg-onboarding-background-300/30`}
isSelected
? "border-custom-primary-100"
: "border-onboarding-border-200 hover:bg-onboarding-background-300/30"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -136,23 +113,35 @@ export const Invitations: React.FC<Props> = (props) => {
<div className="text-sm font-medium">{truncateText(invitedWorkspace?.name, 30)}</div> <div className="text-sm font-medium">{truncateText(invitedWorkspace?.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p> <p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div> </div>
<span className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}> <span className={`flex-shrink-0`}>
<CheckCircle2 className="h-5 w-5" /> <Checkbox checked={isSelected} />
</span> </span>
</div> </div>
); );
})} })}
</div> </div>
<Button variant="primary" size="lg" className="w-full" onClick={submitInvitations}> <Button
{isJoiningWorkspaces ? "Joining..." : "Continue"} variant="primary"
size="lg"
className="w-full"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || !invitationsRespond.length}
>
Continue to workspace
</Button> </Button>
<div className="mx-auto mt-4 flex items-center sm:w-96"> <div className="mx-auto mt-4 flex items-center sm:w-96">
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p> <p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p>
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-onboarding-border-100" />
</div> </div>
<Button variant="link-neutral" size="lg" className="w-full text-base bg-custom-background-90" onClick={handleCurrentViewChange}> <Button
Create my own workspace variant="link-neutral"
size="lg"
className="w-full text-base bg-custom-background-90"
onClick={handleCurrentViewChange}
disabled={isJoiningWorkspaces}
>
Create your own workspace
</Button> </Button>
</div> </div>
) : ( ) : (

View File

@ -16,12 +16,12 @@ import {
import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
// types // types
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; import { IUser, IWorkspace } from "@plane/types";
// ui // ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
import { MEMBER_INVITED } from "@/constants/event-tracker"; import { MEMBER_INVITED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace";
// helpers // helpers
import { getUserRole } from "@/helpers/user.helper"; import { getUserRole } from "@/helpers/user.helper";
// hooks // hooks
@ -39,7 +39,6 @@ import { SwitchOrDeleteAccountDropdown } from "./switch-or-delete-account-dropdo
type Props = { type Props = {
finishOnboarding: () => Promise<void>; finishOnboarding: () => Promise<void>;
totalSteps: number; totalSteps: number;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
user: IUser | undefined; user: IUser | undefined;
workspace: IWorkspace | undefined; workspace: IWorkspace | undefined;
}; };
@ -215,10 +214,10 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
> >
<Listbox.Options <Listbox.Options
ref={dropdownRef} ref={dropdownRef}
className="fixed z-10 mt-1 max-h-48 w-48 overflow-y-auto rounded-md border border-onboarding-border-100 bg-onboarding-background-200 text-xs shadow-lg focus:outline-none" className="fixed z-10 mt-1 h-fit w-48 sm:w-60 overflow-y-auto rounded-md border border-onboarding-border-100 bg-onboarding-background-200 shadow-sm focus:outline-none"
> >
<div className="space-y-1 p-2"> <div className="space-y-1 p-2">
{Object.entries(ROLE).map(([key, value]) => ( {Object.entries(ROLE_DETAILS).map(([key, value]) => (
<Listbox.Option <Listbox.Option
key={key} key={key}
value={parseInt(key)} value={parseInt(key)}
@ -229,9 +228,12 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
} }
> >
{({ selected }) => ( {({ selected }) => (
<div className="flex items-center justify-between gap-2"> <div className="flex items-center text-wrap gap-2 p-1">
<div className="flex items-center gap-2">{value}</div> <div className="flex flex-col">
{selected && <Check className="h-4 w-4 flex-shrink-0" />} <div className="text-sm font-medium">{value.title}</div>
<div className="flex text-xs text-custom-text-300">{value.description}</div>
</div>
{selected && <Check className="h-4 w-4 shrink-0" />}
</div> </div>
)} )}
</Listbox.Option> </Listbox.Option>
@ -264,7 +266,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
}; };
export const InviteMembers: React.FC<Props> = (props) => { export const InviteMembers: React.FC<Props> = (props) => {
const { finishOnboarding, totalSteps, stepChange, workspace } = props; const { finishOnboarding, totalSteps, workspace } = props;
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true); const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
@ -287,11 +289,6 @@ export const InviteMembers: React.FC<Props> = (props) => {
}); });
const nextStep = async () => { const nextStep = async () => {
const payload: Partial<TOnboardingSteps> = {
workspace_invite: true,
};
await stepChange(payload);
await finishOnboarding(); await finishOnboarding();
}; };
@ -371,11 +368,11 @@ export const InviteMembers: React.FC<Props> = (props) => {
<SwitchOrDeleteAccountDropdown /> <SwitchOrDeleteAccountDropdown />
</div> </div>
</div> </div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6"> <div className="flex flex-col w-full items-center justify-center p-8 mt-6 md:w-4/5 mx-auto">
<div className="text-center space-y-1 py-4 mx-auto w-4/5"> <div className="text-center space-y-1 py-4 mx-auto w-4/5">
<h3 className="text-3xl font-bold text-onboarding-text-100">Invite your teammates</h3> <h3 className="text-3xl font-bold text-onboarding-text-100">Invite your teammates</h3>
<p className="font-medium text-onboarding-text-400"> <p className="font-medium text-onboarding-text-400">
Work in plane happens best with your team. Invite them now to use Plane to its potential. Work in plane happens best with your team. Invite them now to use Plane to its potential.
</p> </p>
</div> </div>
<form <form
@ -410,14 +407,14 @@ export const InviteMembers: React.FC<Props> = (props) => {
</div> </div>
<button <button
type="button" type="button"
className="flex items-center mx-8 gap-1.5 bg-transparent text-sm font-semibold text-custom-primary-100 outline-custom-primary-100" className="flex items-center mx-8 gap-1.5 bg-transparent text-sm font-medium text-custom-primary-100 outline-custom-primary-100"
onClick={appendField} onClick={appendField}
> >
<Plus className="h-4 w-4 mb-0.5" strokeWidth={2.5} /> <Plus className="h-4 w-4" strokeWidth={2} />
Add another Add another
</button> </button>
</div> </div>
<div className="flex flex-col mx-auto items-center justify-center gap-4 sm:w-96"> <div className="flex flex-col mx-auto px-8 sm:px-2 items-center justify-center gap-4 w-96">
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"

View File

@ -131,8 +131,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
await Promise.all([ await Promise.all([
updateCurrentUser(userDetailsPayload), updateCurrentUser(userDetailsPayload),
updateUserProfile(profileUpdatePayload), updateUserProfile(profileUpdatePayload),
stepChange({ profile_complete: true }), totalSteps > 2 && stepChange({ profile_complete: true }),
]).then(() => { ]);
captureEvent(USER_DETAILS, { captureEvent(USER_DETAILS, {
state: "SUCCESS", state: "SUCCESS",
element: "Onboarding", element: "Onboarding",
@ -146,7 +146,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
if (totalSteps <= 2) { if (totalSteps <= 2) {
finishOnboarding(); finishOnboarding();
} }
});
} catch { } catch {
captureEvent(USER_DETAILS, { captureEvent(USER_DETAILS, {
state: "FAILED", state: "FAILED",
@ -169,7 +168,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
try { try {
await Promise.all([ await Promise.all([
updateCurrentUser(userDetailsPayload), updateCurrentUser(userDetailsPayload),
formData.password ? handleSetPassword(formData.password) : Promise.resolve(), formData.password && handleSetPassword(formData.password),
]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION)); ]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION));
} catch { } catch {
captureEvent(USER_DETAILS, { captureEvent(USER_DETAILS, {
@ -190,7 +189,10 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
role: formData.role, role: formData.role,
}; };
try { try {
await Promise.all([updateUserProfile(profileUpdatePayload), stepChange({ profile_complete: true })]).then(() => { await Promise.all([
updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }),
]);
captureEvent(USER_DETAILS, { captureEvent(USER_DETAILS, {
state: "SUCCESS", state: "SUCCESS",
element: "Onboarding", element: "Onboarding",
@ -204,7 +206,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
if (totalSteps <= 2) { if (totalSteps <= 2) {
finishOnboarding(); finishOnboarding();
} }
});
} catch { } catch {
captureEvent(USER_DETAILS, { captureEvent(USER_DETAILS, {
state: "FAILED", state: "FAILED",

View File

@ -1,5 +1,5 @@
import { objToQueryParams } from "@/helpers/string.helper";
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "@plane/types"; import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "@plane/types";
import { objToQueryParams } from "@/helpers/string.helper";
const paramsToKey = (params: any) => { const paramsToKey = (params: any) => {
const { const {
@ -76,7 +76,7 @@ const myIssuesParamsToKey = (params: any) => {
export const CURRENT_USER = "CURRENT_USER"; export const CURRENT_USER = "CURRENT_USER";
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
export const USER_WORKSPACES = "USER_WORKSPACES"; export const USER_WORKSPACES_LIST = "USER_WORKSPACES_LIST";
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`; export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`;

View File

@ -1,14 +1,14 @@
// services images // types
import { TStaticViewTypes } from "@plane/types";
// icons
import { SettingIcon } from "@/components/icons/attachment"; import { SettingIcon } from "@/components/icons/attachment";
import { Props } from "@/components/icons/types"; import { Props } from "@/components/icons/types";
// services images
import CSVLogo from "public/services/csv.svg"; import CSVLogo from "public/services/csv.svg";
import ExcelLogo from "public/services/excel.svg"; import ExcelLogo from "public/services/excel.svg";
import GithubLogo from "public/services/github.png"; import GithubLogo from "public/services/github.png";
import JiraLogo from "public/services/jira.svg"; import JiraLogo from "public/services/jira.svg";
import JSONLogo from "public/services/json.svg"; import JSONLogo from "public/services/json.svg";
// types
import { TStaticViewTypes } from "@plane/types";
// icons
export enum EUserWorkspaceRoles { export enum EUserWorkspaceRoles {
GUEST = 5, GUEST = 5,
@ -24,6 +24,25 @@ export const ROLE = {
20: "Admin", 20: "Admin",
}; };
export const ROLE_DETAILS = {
5: {
title: "Guest",
description: "External members of organizations can be invited as guests.",
},
10: {
title: "Viewer",
description: "External members of organizations can be invited as guests.",
},
15: {
title: "Member",
description: "Ability to read, write, edit, and delete entities inside projects, cycles, and modules",
},
20: {
title: "Admin",
description: "All permissions set to true within the workspace.",
},
};
export const ORGANIZATION_SIZE = ["Just myself", "2-10", "11-50", "51-200", "201-500", "500+"]; export const ORGANIZATION_SIZE = ["Just myself", "2-10", "11-50", "51-200", "201-500", "500+"];
export const USER_ROLES = [ export const USER_ROLES = [

View File

@ -6,6 +6,7 @@ import useSWR from "swr";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// hooks // hooks
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { useUser, useUserProfile, useWorkspace } from "@/hooks/store"; import { useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { useCurrentUserSettings } from "@/hooks/store/use-current-user-settings"; import { useCurrentUserSettings } from "@/hooks/store/use-current-user-settings";
@ -34,7 +35,7 @@ export const UserAuthWrapper: FC<IUserAuthWrapper> = observer((props) => {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });
// fetching all workspaces // fetching all workspaces
const { isLoading: workspaceLoader } = useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), { const { isLoading: workspaceLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });

View File

@ -16,6 +16,7 @@ import { EmptyState } from "@/components/common";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
// constants // constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker"; import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { ROLE } from "@/constants/workspace"; import { ROLE } from "@/constants/workspace";
// helpers // helpers
import { truncateText } from "@/helpers/string.helper"; import { truncateText } from "@/helpers/string.helper";
@ -79,7 +80,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
workspaceService workspaceService
.joinWorkspaces({ invitations: invitationsRespond }) .joinWorkspaces({ invitations: invitationsRespond })
.then(() => { .then(() => {
mutate("USER_WORKSPACES"); mutate(USER_WORKSPACES_LIST);
const firstInviteId = invitationsRespond[0]; const firstInviteId = invitationsRespond[0];
const invitation = invitations?.find((i) => i.id === firstInviteId); const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;

View File

@ -9,8 +9,10 @@ import { Spinner } from "@plane/ui";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding"; import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
// hooks // constants
import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker"; import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// hooks
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store"; import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
import useUserAuth from "@/hooks/use-user-auth"; import useUserAuth from "@/hooks/use-user-auth";
// layouts // layouts
@ -24,7 +26,7 @@ import { WorkspaceService } from "@/services/workspace.service";
export enum EOnboardingSteps { export enum EOnboardingSteps {
PROFILE_SETUP = "PROFILE_SETUP", PROFILE_SETUP = "PROFILE_SETUP",
WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN", WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN",
WORKSPACE_INVITE = "WORKSPACE_INVITE", INVITE_MEMBERS = "INVITE_MEMBERS",
} }
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
@ -50,7 +52,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
// computed values // computed values
const workspacesList = Object.values(workspaces ?? {}); const workspacesList = Object.values(workspaces ?? {});
// fetching workspaces list // fetching workspaces list
useSWR(`USER_WORKSPACES_LIST`, () => fetchWorkspaces(), { useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });
// fetching user workspace invitations // fetching user workspace invitations
@ -70,11 +72,25 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
await updateUserProfile(payload); await updateUserProfile(payload);
}; };
// complete onboarding // complete onboarding
const finishOnboarding = async () => { const finishOnboarding = async () => {
if (!user || !workspacesList) return; if (!user || !workspaces) return;
await updateUserOnBoard() const firstWorkspace = Object.values(workspaces ?? {})?.[0];
await Promise.all([
updateUserProfile({
onboarding_step: {
profile_complete: true,
workspace_join: true,
workspace_create: true,
workspace_invite: true,
},
last_workspace_id: firstWorkspace?.id,
}),
updateUserOnBoard(),
])
.then(() => { .then(() => {
captureEvent(USER_ONBOARDING_COMPLETED, { captureEvent(USER_ONBOARDING_COMPLETED, {
// user_role: user.role, // user_role: user.role,
@ -87,7 +103,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
console.log("Failed to update onboarding status"); console.log("Failed to update onboarding status");
}); });
router.replace(`/${workspacesList[0]?.slug}`); router.replace(`/${firstWorkspace?.slug}`);
}; };
useEffect(() => { useEffect(() => {
@ -112,23 +128,6 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
if (!onboardingStep.profile_complete) setStep(EOnboardingSteps.PROFILE_SETUP); if (!onboardingStep.profile_complete) setStep(EOnboardingSteps.PROFILE_SETUP);
if (
!onboardingStep.workspace_join &&
!onboardingStep.workspace_create &&
workspacesList &&
workspacesList?.length > 0
) {
await updateUserProfile({
onboarding_step: {
...profile.onboarding_step,
workspace_join: true,
workspace_create: true,
},
last_workspace_id: workspacesList[0]?.id,
});
return;
}
// For Invited Users, they will skip all other steps. // For Invited Users, they will skip all other steps.
if (totalSteps && totalSteps <= 2) return; if (totalSteps && totalSteps <= 2) return;
@ -141,7 +140,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
(onboardingStep.workspace_join || onboardingStep.workspace_create) && (onboardingStep.workspace_join || onboardingStep.workspace_create) &&
!onboardingStep.workspace_invite !onboardingStep.workspace_invite
) )
setStep(EOnboardingSteps.WORKSPACE_INVITE); setStep(EOnboardingSteps.INVITE_MEMBERS);
}; };
handleStepChange(); handleStepChange();
@ -161,12 +160,16 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
finishOnboarding={finishOnboarding} finishOnboarding={finishOnboarding}
/> />
) : step === EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN ? ( ) : step === EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN ? (
<CreateOrJoinWorkspaces invitations={invitations} totalSteps={totalSteps} stepChange={stepChange} /> <CreateOrJoinWorkspaces
) : step === EOnboardingSteps.WORKSPACE_INVITE ? ( invitations={invitations}
totalSteps={totalSteps}
stepChange={stepChange}
finishOnboarding={finishOnboarding}
/>
) : step === EOnboardingSteps.INVITE_MEMBERS ? (
<InviteMembers <InviteMembers
finishOnboarding={finishOnboarding} finishOnboarding={finishOnboarding}
totalSteps={totalSteps} totalSteps={totalSteps}
stepChange={stepChange}
user={user} user={user}
workspace={workspacesList?.[0]} workspace={workspacesList?.[0]}
/> />

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 167 KiB