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));
|
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"
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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 it’s 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"
|
||||||
|
@ -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",
|
||||||
|
@ -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()}`;
|
||||||
|
|
||||||
|
@ -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 = [
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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 |
Loading…
Reference in New Issue
Block a user