mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: onboarding ui updates and accept invitation workflow updates.
This commit is contained in:
parent
43ce850ae9
commit
4330f0f0c9
@ -58,7 +58,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
const redirectToUniqueCodeLogin = async () => {
|
||||
const redirectToUniqueCodeSignIn = async () => {
|
||||
handleStepChange(EAuthSteps.UNIQUE_CODE);
|
||||
};
|
||||
|
||||
@ -194,7 +194,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
{instance && isSmtpConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={redirectToUniqueCodeLogin}
|
||||
onClick={redirectToUniqueCodeSignIn}
|
||||
variant="outline-primary"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
|
@ -39,19 +39,19 @@ type Props = {
|
||||
mode: EAuthModes;
|
||||
};
|
||||
|
||||
const Titles = {
|
||||
const titles = {
|
||||
[EAuthModes.SIGN_IN]: {
|
||||
[EAuthSteps.EMAIL]: {
|
||||
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]: {
|
||||
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]: {
|
||||
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]: {
|
||||
header: "",
|
||||
@ -61,7 +61,7 @@ const Titles = {
|
||||
[EAuthModes.SIGN_UP]: {
|
||||
[EAuthSteps.EMAIL]: {
|
||||
header: "Create your account",
|
||||
subHeader: "Start tracking your projects with Plane",
|
||||
subHeader: "Start tracking your projects with Plane.",
|
||||
},
|
||||
[EAuthSteps.PASSWORD]: {
|
||||
header: "Create your account",
|
||||
@ -98,7 +98,7 @@ const getHeaderSubHeader = (
|
||||
};
|
||||
}
|
||||
|
||||
return Titles[mode][step];
|
||||
return titles[mode][step];
|
||||
};
|
||||
|
||||
export const AuthRoot = observer((props: Props) => {
|
||||
|
@ -72,10 +72,10 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleRequestNewCode = async () => {
|
||||
const handleRequestNewCode = async (email: string) => {
|
||||
setIsRequestingNewCode(true);
|
||||
|
||||
await handleSendNewCode(uniqueCodeFormData.email)
|
||||
await handleSendNewCode(email)
|
||||
.then(() => setResendCodeTimer(30))
|
||||
.finally(() => setIsRequestingNewCode(false));
|
||||
};
|
||||
@ -86,10 +86,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
}, [csrfToken]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRequestingNewCode(true);
|
||||
handleSendNewCode(email)
|
||||
.then(() => setResendCodeTimer(30))
|
||||
.finally(() => setIsRequestingNewCode(false));
|
||||
handleRequestNewCode(email);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@ -149,7 +146,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRequestNewCode}
|
||||
onClick={() => handleRequestNewCode(uniqueCodeFormData.email)}
|
||||
className={`${
|
||||
isRequestNewCodeDisabled
|
||||
? "text-onboarding-text-400"
|
||||
@ -165,7 +162,14 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
</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}
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -24,10 +24,11 @@ type Props = {
|
||||
invitations: IWorkspaceMemberInvitation[];
|
||||
totalSteps: number;
|
||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||
finishOnboarding: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
|
||||
const { invitations, totalSteps, stepChange } = props;
|
||||
const { invitations, totalSteps, stepChange, finishOnboarding } = props;
|
||||
// states
|
||||
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
|
||||
// store hooks
|
||||
@ -45,14 +46,15 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
|
||||
|
||||
const handleNextStep = async () => {
|
||||
if (!user) return;
|
||||
await stepChange({ workspace_join: true, workspace_create: true });
|
||||
|
||||
await finishOnboarding();
|
||||
};
|
||||
|
||||
return (
|
||||
<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="flex items-center justify-between">
|
||||
<OnboardingHeader currentStep={2} totalSteps={totalSteps} />
|
||||
<OnboardingHeader currentStep={totalSteps - 1} totalSteps={totalSteps} />
|
||||
<div className="shrink-0 lg:hidden">
|
||||
<SwitchOrDeleteAccountDropdown />
|
||||
</div>
|
||||
|
@ -1,25 +1,23 @@
|
||||
import React, { useState } from "react";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// icons
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import useSWR from "swr";;
|
||||
// types
|
||||
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Checkbox } from "@plane/ui";
|
||||
// constants
|
||||
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";
|
||||
// helpers
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
type Props = {
|
||||
handleNextStep: () => void;
|
||||
handleNextStep: () => Promise<void>;
|
||||
handleCurrentViewChange: () => void;
|
||||
};
|
||||
const workspaceService = new WorkspaceService();
|
||||
@ -31,14 +29,9 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { updateCurrentUser } = useUser();
|
||||
const { workspaces, fetchWorkspaces } = useWorkspace();
|
||||
const { fetchWorkspaces } = useWorkspace();
|
||||
|
||||
const workspacesList = Object.values(workspaces);
|
||||
|
||||
const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||
workspaceService.userWorkspaceInvitations()
|
||||
);
|
||||
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
|
||||
|
||||
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
|
||||
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 invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]);
|
||||
|
||||
@ -62,42 +48,37 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||
|
||||
setIsJoiningWorkspaces(true);
|
||||
|
||||
await workspaceService
|
||||
.joinWorkspaces({ invitations: invitationsRespond })
|
||||
.then(async () => {
|
||||
captureEvent(MEMBER_ACCEPTED, {
|
||||
member_id: invitation?.id,
|
||||
role: getUserRole(invitation?.role as any),
|
||||
project_id: undefined,
|
||||
accepted_from: "App",
|
||||
state: "SUCCESS",
|
||||
element: "Workspace invitations page",
|
||||
});
|
||||
await fetchWorkspaces();
|
||||
await mutate(USER_WORKSPACES);
|
||||
await updateLastWorkspace();
|
||||
await handleNextStep();
|
||||
await mutateInvitations();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
captureEvent(MEMBER_ACCEPTED, {
|
||||
member_id: invitation?.id,
|
||||
role: getUserRole(invitation?.role as any),
|
||||
project_id: undefined,
|
||||
accepted_from: "App",
|
||||
state: "FAILED",
|
||||
element: "Workspace invitations page",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsJoiningWorkspaces(false));
|
||||
try {
|
||||
await workspaceService.joinWorkspaces({ invitations: invitationsRespond });
|
||||
captureEvent(MEMBER_ACCEPTED, {
|
||||
member_id: invitation?.id,
|
||||
role: getUserRole(invitation?.role as any),
|
||||
project_id: undefined,
|
||||
accepted_from: "App",
|
||||
state: "SUCCESS",
|
||||
element: "Workspace invitations page",
|
||||
});
|
||||
await fetchWorkspaces();
|
||||
await handleNextStep();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
captureEvent(MEMBER_ACCEPTED, {
|
||||
member_id: invitation?.id,
|
||||
role: getUserRole(invitation?.role as any),
|
||||
project_id: undefined,
|
||||
accepted_from: "App",
|
||||
state: "FAILED",
|
||||
element: "Workspace invitations page",
|
||||
});
|
||||
setIsJoiningWorkspaces(false);
|
||||
}
|
||||
};
|
||||
|
||||
return invitations && invitations.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<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>
|
||||
{invitations &&
|
||||
@ -108,11 +89,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div
|
||||
key={invitation.id}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 ${
|
||||
isSelected
|
||||
? "border-custom-primary-100"
|
||||
: "border-onboarding-border-200 hover:bg-onboarding-background-300/30"
|
||||
}`}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 border-custom-border-200 hover:bg-onboarding-background-300/30`}
|
||||
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
||||
>
|
||||
<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>
|
||||
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
|
||||
</div>
|
||||
<span className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}>
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
<span className={`flex-shrink-0`}>
|
||||
<Checkbox checked={isSelected} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button variant="primary" size="lg" className="w-full" onClick={submitInvitations}>
|
||||
{isJoiningWorkspaces ? "Joining..." : "Continue"}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={submitInvitations}
|
||||
disabled={isJoiningWorkspaces || !invitationsRespond.length}
|
||||
>
|
||||
Continue to workspace
|
||||
</Button>
|
||||
<div className="mx-auto mt-4 flex items-center sm:w-96">
|
||||
<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>
|
||||
<hr className="w-full border-onboarding-border-100" />
|
||||
</div>
|
||||
<Button variant="link-neutral" size="lg" className="w-full text-base bg-custom-background-90" onClick={handleCurrentViewChange}>
|
||||
Create my own workspace
|
||||
<Button
|
||||
variant="link-neutral"
|
||||
size="lg"
|
||||
className="w-full text-base bg-custom-background-90"
|
||||
onClick={handleCurrentViewChange}
|
||||
disabled={isJoiningWorkspaces}
|
||||
>
|
||||
Create your own workspace
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -16,12 +16,12 @@ import {
|
||||
import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||
import { IUser, IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { MEMBER_INVITED } from "@/constants/event-tracker";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
@ -39,7 +39,6 @@ import { SwitchOrDeleteAccountDropdown } from "./switch-or-delete-account-dropdo
|
||||
type Props = {
|
||||
finishOnboarding: () => Promise<void>;
|
||||
totalSteps: number;
|
||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||
user: IUser | undefined;
|
||||
workspace: IWorkspace | undefined;
|
||||
};
|
||||
@ -215,10 +214,10 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
|
||||
>
|
||||
<Listbox.Options
|
||||
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">
|
||||
{Object.entries(ROLE).map(([key, value]) => (
|
||||
{Object.entries(ROLE_DETAILS).map(([key, value]) => (
|
||||
<Listbox.Option
|
||||
key={key}
|
||||
value={parseInt(key)}
|
||||
@ -229,9 +228,12 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{value}</div>
|
||||
{selected && <Check className="h-4 w-4 flex-shrink-0" />}
|
||||
<div className="flex items-center text-wrap gap-2 p-1">
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
@ -264,7 +266,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
|
||||
};
|
||||
|
||||
export const InviteMembers: React.FC<Props> = (props) => {
|
||||
const { finishOnboarding, totalSteps, stepChange, workspace } = props;
|
||||
const { finishOnboarding, totalSteps, workspace } = props;
|
||||
|
||||
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
|
||||
|
||||
@ -287,11 +289,6 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
const nextStep = async () => {
|
||||
const payload: Partial<TOnboardingSteps> = {
|
||||
workspace_invite: true,
|
||||
};
|
||||
|
||||
await stepChange(payload);
|
||||
await finishOnboarding();
|
||||
};
|
||||
|
||||
@ -371,11 +368,11 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
||||
<SwitchOrDeleteAccountDropdown />
|
||||
</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">
|
||||
<h3 className="text-3xl font-bold text-onboarding-text-100">Invite your teammates</h3>
|
||||
<p className="font-medium text-onboarding-text-400">
|
||||
Work in plane happens best with your team. Invite them now to use Plane to it’s potential.
|
||||
Work in plane happens best with your team. Invite them now to use Plane to its potential.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
@ -410,14 +407,14 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<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}
|
||||
>
|
||||
<Plus className="h-4 w-4 mb-0.5" strokeWidth={2.5} />
|
||||
<Plus className="h-4 w-4" strokeWidth={2} />
|
||||
Add another
|
||||
</button>
|
||||
</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
|
||||
variant="primary"
|
||||
type="submit"
|
||||
|
@ -131,22 +131,21 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
await Promise.all([
|
||||
updateCurrentUser(userDetailsPayload),
|
||||
updateUserProfile(profileUpdatePayload),
|
||||
stepChange({ profile_complete: true }),
|
||||
]).then(() => {
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "SUCCESS",
|
||||
element: "Onboarding",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Profile setup completed!",
|
||||
});
|
||||
// For Invited Users, they will skip all other steps and finish onboarding.
|
||||
if (totalSteps <= 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
totalSteps > 2 && stepChange({ profile_complete: true }),
|
||||
]);
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "SUCCESS",
|
||||
element: "Onboarding",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Profile setup completed!",
|
||||
});
|
||||
// For Invited Users, they will skip all other steps and finish onboarding.
|
||||
if (totalSteps <= 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
} catch {
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "FAILED",
|
||||
@ -169,7 +168,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
updateCurrentUser(userDetailsPayload),
|
||||
formData.password ? handleSetPassword(formData.password) : Promise.resolve(),
|
||||
formData.password && handleSetPassword(formData.password),
|
||||
]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION));
|
||||
} catch {
|
||||
captureEvent(USER_DETAILS, {
|
||||
@ -190,21 +189,23 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
role: formData.role,
|
||||
};
|
||||
try {
|
||||
await Promise.all([updateUserProfile(profileUpdatePayload), stepChange({ profile_complete: true })]).then(() => {
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "SUCCESS",
|
||||
element: "Onboarding",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Profile setup completed!",
|
||||
});
|
||||
// For Invited Users, they will skip all other steps and finish onboarding.
|
||||
if (totalSteps <= 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
await Promise.all([
|
||||
updateUserProfile(profileUpdatePayload),
|
||||
totalSteps > 2 && stepChange({ profile_complete: true }),
|
||||
]);
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "SUCCESS",
|
||||
element: "Onboarding",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Profile setup completed!",
|
||||
});
|
||||
// For Invited Users, they will skip all other steps and finish onboarding.
|
||||
if (totalSteps <= 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
} catch {
|
||||
captureEvent(USER_DETAILS, {
|
||||
state: "FAILED",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { objToQueryParams } from "@/helpers/string.helper";
|
||||
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "@plane/types";
|
||||
import { objToQueryParams } from "@/helpers/string.helper";
|
||||
|
||||
const paramsToKey = (params: any) => {
|
||||
const {
|
||||
@ -76,7 +76,7 @@ const myIssuesParamsToKey = (params: any) => {
|
||||
|
||||
export const CURRENT_USER = "CURRENT_USER";
|
||||
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()}`;
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
// services images
|
||||
// types
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
// icons
|
||||
import { SettingIcon } from "@/components/icons/attachment";
|
||||
import { Props } from "@/components/icons/types";
|
||||
// services images
|
||||
import CSVLogo from "public/services/csv.svg";
|
||||
import ExcelLogo from "public/services/excel.svg";
|
||||
import GithubLogo from "public/services/github.png";
|
||||
import JiraLogo from "public/services/jira.svg";
|
||||
import JSONLogo from "public/services/json.svg";
|
||||
// types
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
// icons
|
||||
|
||||
export enum EUserWorkspaceRoles {
|
||||
GUEST = 5,
|
||||
@ -24,6 +24,25 @@ export const ROLE = {
|
||||
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 USER_ROLES = [
|
||||
|
@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||
import { useUser, useUserProfile, useWorkspace } from "@/hooks/store";
|
||||
import { useCurrentUserSettings } from "@/hooks/store/use-current-user-settings";
|
||||
|
||||
@ -34,7 +35,7 @@ export const UserAuthWrapper: FC<IUserAuthWrapper> = observer((props) => {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
// fetching all workspaces
|
||||
const { isLoading: workspaceLoader } = useSWR("USER_WORKSPACES_LIST", () => fetchWorkspaces(), {
|
||||
const { isLoading: workspaceLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
|
@ -16,6 +16,7 @@ import { EmptyState } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
|
||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||
import { ROLE } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
@ -79,7 +80,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
||||
workspaceService
|
||||
.joinWorkspaces({ invitations: invitationsRespond })
|
||||
.then(() => {
|
||||
mutate("USER_WORKSPACES");
|
||||
mutate(USER_WORKSPACES_LIST);
|
||||
const firstInviteId = invitationsRespond[0];
|
||||
const invitation = invitations?.find((i) => i.id === firstInviteId);
|
||||
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
|
||||
|
@ -9,8 +9,10 @@ import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
|
||||
// hooks
|
||||
// constants
|
||||
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 useUserAuth from "@/hooks/use-user-auth";
|
||||
// layouts
|
||||
@ -24,7 +26,7 @@ import { WorkspaceService } from "@/services/workspace.service";
|
||||
export enum EOnboardingSteps {
|
||||
PROFILE_SETUP = "PROFILE_SETUP",
|
||||
WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN",
|
||||
WORKSPACE_INVITE = "WORKSPACE_INVITE",
|
||||
INVITE_MEMBERS = "INVITE_MEMBERS",
|
||||
}
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
@ -50,7 +52,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
// computed values
|
||||
const workspacesList = Object.values(workspaces ?? {});
|
||||
// fetching workspaces list
|
||||
useSWR(`USER_WORKSPACES_LIST`, () => fetchWorkspaces(), {
|
||||
useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
// fetching user workspace invitations
|
||||
@ -70,11 +72,25 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
await updateUserProfile(payload);
|
||||
};
|
||||
|
||||
// complete onboarding
|
||||
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(() => {
|
||||
captureEvent(USER_ONBOARDING_COMPLETED, {
|
||||
// user_role: user.role,
|
||||
@ -87,7 +103,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
console.log("Failed to update onboarding status");
|
||||
});
|
||||
|
||||
router.replace(`/${workspacesList[0]?.slug}`);
|
||||
router.replace(`/${firstWorkspace?.slug}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -112,23 +128,6 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
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.
|
||||
if (totalSteps && totalSteps <= 2) return;
|
||||
|
||||
@ -141,7 +140,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
(onboardingStep.workspace_join || onboardingStep.workspace_create) &&
|
||||
!onboardingStep.workspace_invite
|
||||
)
|
||||
setStep(EOnboardingSteps.WORKSPACE_INVITE);
|
||||
setStep(EOnboardingSteps.INVITE_MEMBERS);
|
||||
};
|
||||
|
||||
handleStepChange();
|
||||
@ -161,12 +160,16 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
finishOnboarding={finishOnboarding}
|
||||
/>
|
||||
) : step === EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN ? (
|
||||
<CreateOrJoinWorkspaces invitations={invitations} totalSteps={totalSteps} stepChange={stepChange} />
|
||||
) : step === EOnboardingSteps.WORKSPACE_INVITE ? (
|
||||
<CreateOrJoinWorkspaces
|
||||
invitations={invitations}
|
||||
totalSteps={totalSteps}
|
||||
stepChange={stepChange}
|
||||
finishOnboarding={finishOnboarding}
|
||||
/>
|
||||
) : step === EOnboardingSteps.INVITE_MEMBERS ? (
|
||||
<InviteMembers
|
||||
finishOnboarding={finishOnboarding}
|
||||
totalSteps={totalSteps}
|
||||
stepChange={stepChange}
|
||||
user={user}
|
||||
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 |
Loading…
Reference in New Issue
Block a user