forked from github/plane
Authentication Workflow fixes. Redirection fixes (#832)
* auth integration fixes * auth integration fixes * auth integration fixes * auth integration fixes * dev: update user api to return fallback workspace and improve the structure of the response * dev: fix the issue keyerror and move onboarding logic to serializer method field * dev: use-user-auth hook imlemented for route access validation and build issues resolved effected by user payload * fix: global theme color fix * style: new onboarding ui , fix: use-user-auth hook implemented * fix: command palette, project invite modal and issue detail page mutation type fix * fix: onboarding redirection fix * dev: build isuue resolved * fix: use user auth hook fix * fix: sign in toast alert fix, sign out redirection fix and user theme error fix * fix: user response fix * fix: unAuthorizedStatus logic updated --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: anmolsinghbhatia <anmolsinghbhatia@caravel.tech>
This commit is contained in:
parent
33db616767
commit
44f8ba407d
@ -25,6 +25,10 @@ class UserSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
extra_kwargs = {"password": {"write_only": True}}
|
extra_kwargs = {"password": {"write_only": True}}
|
||||||
|
|
||||||
|
# If the user has already filled first name or last name then he is onboarded
|
||||||
|
def get_is_onboarded(self, obj):
|
||||||
|
return bool(obj.first_name) or bool(obj.last_name)
|
||||||
|
|
||||||
|
|
||||||
class UserLiteSerializer(BaseSerializer):
|
class UserLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -31,36 +31,61 @@ class UserEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
def retrieve(self, request):
|
def retrieve(self, request):
|
||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(pk=request.user.last_workspace_id)
|
workspace = Workspace.objects.get(
|
||||||
|
pk=request.user.last_workspace_id, workspace_member__member=request.user
|
||||||
|
)
|
||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
email=request.user.email
|
email=request.user.email
|
||||||
).count()
|
).count()
|
||||||
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
|
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
|
||||||
|
|
||||||
|
serialized_data = UserSerializer(request.user).data
|
||||||
|
serialized_data["workspace"] = {
|
||||||
|
"last_workspace_id": request.user.last_workspace_id,
|
||||||
|
"last_workspace_slug": workspace.slug,
|
||||||
|
"fallback_workspace_id": request.user.last_workspace_id,
|
||||||
|
"fallback_workspace_slug": workspace.slug,
|
||||||
|
"invites": workspace_invites,
|
||||||
|
}
|
||||||
|
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
serialized_data,
|
||||||
"user": UserSerializer(request.user).data,
|
|
||||||
"slug": workspace.slug,
|
|
||||||
"workspace_invites": workspace_invites,
|
|
||||||
"assigned_issues": assigned_issues,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Workspace.DoesNotExist:
|
except Workspace.DoesNotExist:
|
||||||
|
# This exception will be hit even when the `last_workspace_id` is None
|
||||||
|
|
||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
email=request.user.email
|
email=request.user.email
|
||||||
).count()
|
).count()
|
||||||
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
|
assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
|
||||||
|
|
||||||
|
fallback_workspace = Workspace.objects.filter(
|
||||||
|
workspace_member__member=request.user
|
||||||
|
).order_by("created_at").first()
|
||||||
|
|
||||||
|
serialized_data = UserSerializer(request.user).data
|
||||||
|
|
||||||
|
serialized_data["workspace"] = {
|
||||||
|
"last_workspace_id": None,
|
||||||
|
"last_workspace_slug": None,
|
||||||
|
"fallback_workspace_id": fallback_workspace.id
|
||||||
|
if fallback_workspace is not None
|
||||||
|
else None,
|
||||||
|
"fallback_workspace_slug": fallback_workspace.slug
|
||||||
|
if fallback_workspace is not None
|
||||||
|
else None,
|
||||||
|
"invites": workspace_invites,
|
||||||
|
}
|
||||||
|
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
serialized_data,
|
||||||
"user": UserSerializer(request.user).data,
|
|
||||||
"slug": None,
|
|
||||||
"workspace_invites": workspace_invites,
|
|
||||||
"assigned_issues": assigned_issues,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
@ -66,8 +66,12 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||||||
const handleSignin = async (formData: EmailCodeFormValues) => {
|
const handleSignin = async (formData: EmailCodeFormValues) => {
|
||||||
await authenticationService
|
await authenticationService
|
||||||
.magicSignIn(formData)
|
.magicSignIn(formData)
|
||||||
.then((response) => {
|
.then(() => {
|
||||||
onSuccess(response);
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: "Successfully logged in!",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import { useState, FC } from "react";
|
|
||||||
import { KeyIcon } from "@heroicons/react/24/outline";
|
|
||||||
// components
|
|
||||||
import { EmailCodeForm, EmailPasswordForm } from "components/account";
|
|
||||||
|
|
||||||
export interface EmailSignInFormProps {
|
|
||||||
handleSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmailSignInForm: FC<EmailSignInFormProps> = (props) => {
|
|
||||||
const { handleSuccess } = props;
|
|
||||||
// states
|
|
||||||
const [useCode, setUseCode] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{useCode ? (
|
|
||||||
<EmailCodeForm onSuccess={handleSuccess} />
|
|
||||||
) : (
|
|
||||||
<EmailPasswordForm onSuccess={handleSuccess} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -29,7 +29,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const origin =
|
const origin =
|
||||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
setLoginCallBackURL(`${origin}/signin` as any);
|
setLoginCallBackURL(`${origin}/` as any);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,4 +2,3 @@ export * from "./google-login";
|
|||||||
export * from "./email-code-form";
|
export * from "./email-code-form";
|
||||||
export * from "./email-password-form";
|
export * from "./email-password-form";
|
||||||
export * from "./github-login-button";
|
export * from "./github-login-button";
|
||||||
export * from "./email-signin-form";
|
|
||||||
|
@ -39,7 +39,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
{user ? (
|
{user ? (
|
||||||
<p>
|
<p>
|
||||||
You have signed in as {user.email}. <br />
|
You have signed in as {user.email}. <br />
|
||||||
<Link href={`/signin?next=${currentPath}`}>
|
<Link href={`/?next=${currentPath}`}>
|
||||||
<a className="font-medium text-brand-base">Sign in</a>
|
<a className="font-medium text-brand-base">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with different account that has access to this page.
|
with different account that has access to this page.
|
||||||
@ -47,7 +47,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
You need to{" "}
|
You need to{" "}
|
||||||
<Link href={`/signin?next=${currentPath}`}>
|
<Link href={`/?next=${currentPath}`}>
|
||||||
<a className="font-medium text-brand-base">Sign in</a>
|
<a className="font-medium text-brand-base">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with an account that has access to this page.
|
with an account that has access to this page.
|
||||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
|
||||||
type BreadcrumbsProps = {
|
type BreadcrumbsProps = {
|
||||||
children: any;
|
children: any;
|
||||||
@ -16,10 +17,13 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-3 w-3" />
|
<Icon
|
||||||
|
iconName="keyboard_backspace"
|
||||||
|
className="text-base leading-4 text-brand-secondary group-hover:text-brand-base"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,12 +57,15 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData: IIssue) => ({
|
async (prevData) => {
|
||||||
...prevData,
|
if (!prevData) return prevData;
|
||||||
...formData,
|
return {
|
||||||
}),
|
...prevData,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAssignees = (assignee: string) => {
|
const handleIssueAssignees = (assignee: string) => {
|
||||||
const updatedAssignees = issue.assignees ?? [];
|
const updatedAssignees = issue.assignees_list ?? [];
|
||||||
|
|
||||||
if (updatedAssignees.includes(assignee)) {
|
if (updatedAssignees.includes(assignee)) {
|
||||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||||
|
@ -27,12 +27,16 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData: IIssue) => ({
|
async (prevData) => {
|
||||||
...prevData,
|
if (!prevData) return prevData;
|
||||||
...formData,
|
|
||||||
}),
|
return {
|
||||||
|
...prevData,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -39,12 +39,15 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData: IIssue) => ({
|
async (prevData) => {
|
||||||
...prevData,
|
if (!prevData) return prevData;
|
||||||
...formData,
|
return {
|
||||||
}),
|
...prevData,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -120,12 +120,17 @@ export const CommandPalette: React.FC = () => {
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData: IIssue) => ({
|
|
||||||
...prevData,
|
(prevData) => {
|
||||||
...formData,
|
if (!prevData) return prevData;
|
||||||
}),
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -45,8 +45,9 @@ export const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full max-w-xl flex-col gap-12">
|
<div className="flex w-full max-w-xl flex-col gap-12">
|
||||||
<div className="flex flex-col gap-6 rounded-[10px] bg-brand-base p-7 shadow-md">
|
<div className="flex flex-col gap-6 rounded-[10px] bg-brand-base p-7 shadow-md">
|
||||||
<h2 className="text-xl font-medium">Invite your team to your workspace.</h2>
|
<h2 className="text-xl font-medium">Invite co-workers to your team</h2>
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
|
<span>Email</span>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<MultiInput
|
<MultiInput
|
||||||
name="emails"
|
name="emails"
|
||||||
|
@ -16,7 +16,7 @@ type Props = {
|
|||||||
export const OnboardingCard: React.FC<Props> = ({ data, gradient = false }) => (
|
export const OnboardingCard: React.FC<Props> = ({ data, gradient = false }) => (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center justify-center gap-7 rounded-[10px] px-14 pt-10 text-center ${
|
className={`flex flex-col items-center justify-center gap-7 rounded-[10px] px-14 pt-10 text-center ${
|
||||||
gradient ? "bg-gradient-to-b from-[#2C8DFF]/50 via-brand-base to-transparent" : ""
|
gradient ? "bg-gradient-to-b from-[#C1DDFF] via-brand-base to-transparent" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-44 w-full">
|
<div className="h-44 w-full">
|
||||||
|
@ -66,10 +66,17 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="flex w-full max-w-xl flex-col gap-12">
|
<div className="flex w-full max-w-xl flex-col gap-7">
|
||||||
<div className="flex flex-col rounded-[10px] bg-brand-base shadow-md">
|
<div className="flex flex-col rounded-[10px] bg-brand-base shadow-md">
|
||||||
<div className="flex flex-col justify-between gap-3 px-10 py-7 sm:flex-row">
|
<div className="flex flex-col gap-2 justify-center px-7 pt-7 pb-3.5">
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
<h3 className="text-base font-semibold text-brand-base">User Details</h3>
|
||||||
|
<p className="text-sm text-brand-secondary">
|
||||||
|
Enter your details as a first step to open your Plane account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between gap-4 px-7 py-3.5 sm:flex-row">
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5 w-full sm:w-1/2">
|
||||||
<span>First name</span>
|
<span>First name</span>
|
||||||
<Input
|
<Input
|
||||||
name="first_name"
|
name="first_name"
|
||||||
@ -81,7 +88,7 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||||||
error={errors.first_name}
|
error={errors.first_name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
<div className="flex flex-col items-start justify-center gap-2.5 w-full sm:w-1/2">
|
||||||
<span>Last name</span>
|
<span>Last name</span>
|
||||||
<Input
|
<Input
|
||||||
name="last_name"
|
name="last_name"
|
||||||
@ -94,7 +101,8 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-brand-base px-10 py-7">
|
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-brand-base px-7 pt-3.5 pb-7">
|
||||||
<span>What is your role?</span>
|
<span>What is your role?</span>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Controller
|
<Controller
|
||||||
@ -123,6 +131,7 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-center ">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -16,6 +16,7 @@ import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
|||||||
import { CreateWorkspaceForm } from "components/workspace";
|
import { CreateWorkspaceForm } from "components/workspace";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
|
import { getFirstCharacters, truncateText } from "helpers/string.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
@ -30,6 +31,7 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
slug: "",
|
slug: "",
|
||||||
company_size: null,
|
company_size: null,
|
||||||
});
|
});
|
||||||
|
const [currentTab, setCurrentTab] = useState("create");
|
||||||
|
|
||||||
const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||||
workspaceService.userWorkspaceInvitations()
|
workspaceService.userWorkspaceInvitations()
|
||||||
@ -64,53 +66,72 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentTabValue = (tab: string | null) => {
|
||||||
|
switch (tab) {
|
||||||
|
case "join":
|
||||||
|
return 0;
|
||||||
|
case "create":
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("invitations:", invitations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-[490px] w-full place-items-center">
|
<div className="grid w-full place-items-center">
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
as="div"
|
as="div"
|
||||||
className="flex h-full w-full max-w-xl flex-col justify-between rounded-[10px] bg-brand-base shadow-md"
|
className="flex h-[417px] w-full max-w-xl flex-col justify-between rounded-[10px] bg-brand-base shadow-md"
|
||||||
|
defaultIndex={currentTabValue(currentTab)}
|
||||||
|
onChange={(i) => {
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
return setCurrentTab("join");
|
||||||
|
case 1:
|
||||||
|
return setCurrentTab("create");
|
||||||
|
default:
|
||||||
|
return setCurrentTab("create");
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tab.List
|
<Tab.List as="div" className="flex flex-col gap-3 px-7 pt-7 pb-3.5">
|
||||||
as="div"
|
<div className="flex flex-col gap-2 justify-center">
|
||||||
className="text-gray-8 flex items-center justify-start gap-3 px-4 pt-4 text-sm"
|
<h3 className="text-base font-semibold text-brand-base">Workspaces</h3>
|
||||||
>
|
<p className="text-sm text-brand-secondary">
|
||||||
<Tab
|
Create or join the workspace to get started with Plane.
|
||||||
className={({ selected }) =>
|
</p>
|
||||||
`rounded-3xl border px-4 py-2 outline-none ${
|
</div>
|
||||||
selected
|
<div className="text-gray-8 flex items-center justify-start gap-3 text-sm">
|
||||||
? "border-brand-accent bg-brand-accent text-white"
|
<Tab
|
||||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
className={({ selected }) =>
|
||||||
}`
|
`rounded-3xl border px-4 py-2 outline-none ${
|
||||||
}
|
selected
|
||||||
>
|
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||||
New Workspace
|
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||||
</Tab>
|
}`
|
||||||
<Tab
|
}
|
||||||
className={({ selected }) =>
|
>
|
||||||
`rounded-3xl border px-5 py-2 outline-none ${
|
Invited Workspace
|
||||||
selected
|
</Tab>
|
||||||
? "border-brand-accent bg-brand-accent text-white"
|
<Tab
|
||||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
className={({ selected }) =>
|
||||||
}`
|
`rounded-3xl border px-4 py-2 outline-none ${
|
||||||
}
|
selected
|
||||||
>
|
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||||
Invited Workspace
|
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||||
</Tab>
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Workspace
|
||||||
|
</Tab>
|
||||||
|
</div>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels as="div" className="h-full">
|
<Tab.Panels as="div" className="h-full">
|
||||||
<Tab.Panel>
|
|
||||||
<CreateWorkspaceForm
|
|
||||||
onSubmit={(res) => {
|
|
||||||
setWorkspace(res);
|
|
||||||
setStep(3);
|
|
||||||
}}
|
|
||||||
defaultValues={defaultValues}
|
|
||||||
setDefaultValues={setDefaultValues}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="h-full">
|
<Tab.Panel className="h-full">
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<div className="flex h-full w-full flex-col">
|
||||||
<div className="divide-y px-4 py-7">
|
<div className="h-[255px] overflow-y-auto px-7">
|
||||||
{invitations && invitations.length > 0 ? (
|
{invitations && invitations.length > 0 ? (
|
||||||
invitations.map((invitation) => (
|
invitations.map((invitation) => (
|
||||||
<div key={invitation.id}>
|
<div key={invitation.id}>
|
||||||
@ -129,34 +150,62 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
alt={invitation.workspace.name}
|
alt={invitation.workspace.name}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-full w-full items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
<span className="flex h-full w-full items-center justify-center rounded-xl bg-gray-700 p-4 uppercase text-white">
|
||||||
{invitation.workspace.name.charAt(0)}
|
{getFirstCharacters(invitation.workspace.name)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium">{invitation.workspace.name}</div>
|
<div className="text-sm font-medium">
|
||||||
|
{truncateText(invitation.workspace.name, 30)}
|
||||||
|
</div>
|
||||||
<p className="text-sm text-brand-secondary">
|
<p className="text-sm text-brand-secondary">
|
||||||
Invited by {invitation.workspace.owner.first_name}
|
Invited by {invitation.workspace.owner.first_name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<input
|
<button
|
||||||
id={invitation.id}
|
className={`${
|
||||||
aria-describedby="workspaces"
|
invitationsRespond.includes(invitation.id)
|
||||||
name={invitation.id}
|
? "bg-brand-surface-2 text-brand-secondary"
|
||||||
checked={invitationsRespond.includes(invitation.id)}
|
: "bg-brand-accent text-white"
|
||||||
value={invitation.workspace.name}
|
} text-sm px-4 py-2 border border-brand-base rounded-3xl`}
|
||||||
onChange={(e) => {
|
onClick={(e) => {
|
||||||
handleInvitation(
|
handleInvitation(
|
||||||
invitation,
|
invitation,
|
||||||
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
>
|
||||||
className="h-4 w-4 rounded border-brand-base text-brand-accent focus:ring-brand-accent"
|
{invitationsRespond.includes(invitation.id)
|
||||||
/>
|
? "Invitation Accepted"
|
||||||
|
: "Accept Invitation"}
|
||||||
|
</button>
|
||||||
|
{/* <input
|
||||||
|
id={invitation.id}
|
||||||
|
aria-describedby="workspaces"
|
||||||
|
name={invitation.id}
|
||||||
|
value={
|
||||||
|
invitationsRespond.includes(invitation.id)
|
||||||
|
? "Invitation Accepted"
|
||||||
|
: "Accept Invitation"
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
handleInvitation(
|
||||||
|
invitation,
|
||||||
|
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
invitationsRespond.includes(invitation.id)
|
||||||
|
? "bg-brand-surface-2 text-brand-secondary"
|
||||||
|
: "bg-brand-accent text-white"
|
||||||
|
} text-sm px-4 py-2 border border-brand-base rounded-3xl`}
|
||||||
|
|
||||||
|
// className="h-4 w-4 rounded border-brand-base text-brand-accent focus:ring-brand-accent"
|
||||||
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -167,7 +216,7 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-center rounded-b-[10px] py-7">
|
<div className="flex w-full items-center justify-center rounded-b-[10px] pt-10">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-1/2 text-center"
|
className="w-1/2 text-center"
|
||||||
@ -180,6 +229,16 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="h-full">
|
||||||
|
<CreateWorkspaceForm
|
||||||
|
onSubmit={(res) => {
|
||||||
|
setWorkspace(res);
|
||||||
|
setStep(3);
|
||||||
|
}}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
setDefaultValues={setDefaultValues}
|
||||||
|
/>
|
||||||
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,9 +73,12 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
|||||||
.inviteProject(workspaceSlug as string, projectId as string, formData)
|
.inviteProject(workspaceSlug as string, projectId as string, formData)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
mutate(
|
mutate<any[]>(
|
||||||
PROJECT_INVITATIONS,
|
PROJECT_INVITATIONS,
|
||||||
(prevData: any[]) => [{ ...formData, ...response }, ...(prevData ?? [])],
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return [{ ...formData, ...response }, ...(prevData ?? [])];
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
12
apps/app/components/ui/icon.tsx
Normal file
12
apps/app/components/ui/icon.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
iconName: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||||
|
<span className={`material-symbols-rounded text-lg leading-5 font-light ${className}`}>
|
||||||
|
{iconName}
|
||||||
|
</span>
|
||||||
|
);
|
@ -25,3 +25,4 @@ export * from "./markdown-to-component";
|
|||||||
export * from "./product-updates-modal";
|
export * from "./product-updates-modal";
|
||||||
export * from "./integration-and-import-export-banner";
|
export * from "./integration-and-import-export-banner";
|
||||||
export * from "./range-datepicker";
|
export * from "./range-datepicker";
|
||||||
|
export * from "./icon";
|
||||||
|
@ -99,110 +99,105 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="flex h-full w-full flex-col" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||||
className="flex w-full items-center justify-center"
|
<div className="divide-y h-[255px]">
|
||||||
onSubmit={handleSubmit(handleCreateWorkspace)}
|
<div className="flex flex-col justify-between gap-3.5 px-7 pb-3.5">
|
||||||
>
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
<div className="flex w-full max-w-xl flex-col">
|
<span className="text-sm">Workspace name</span>
|
||||||
<div className="flex flex-col rounded-[10px] bg-brand-base">
|
<Input
|
||||||
<div className="flex flex-col justify-between gap-3 px-4 py-7">
|
name="name"
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
register={register}
|
||||||
<span className="text-sm">Workspace name</span>
|
autoComplete="off"
|
||||||
|
onChange={(e) =>
|
||||||
|
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
|
||||||
|
}
|
||||||
|
validations={{
|
||||||
|
required: "Workspace name is required",
|
||||||
|
validate: (value) =>
|
||||||
|
/^[\w\s-]*$/.test(value) ||
|
||||||
|
`Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`,
|
||||||
|
}}
|
||||||
|
placeholder="e.g. My Workspace"
|
||||||
|
className="placeholder:text-brand-secondary"
|
||||||
|
error={errors.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
|
<span className="text-sm">Workspace URL</span>
|
||||||
|
<div className="flex w-full items-center rounded-md border border-brand-base px-3">
|
||||||
|
<span className="whitespace-nowrap text-sm text-brand-secondary">
|
||||||
|
{typeof window !== "undefined" && window.location.origin}/
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
name="name"
|
mode="trueTransparent"
|
||||||
register={register}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={(e) =>
|
name="slug"
|
||||||
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
|
register={register}
|
||||||
}
|
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
|
||||||
validations={{
|
validations={{
|
||||||
required: "Workspace name is required",
|
required: "Workspace URL is required",
|
||||||
validate: (value) =>
|
|
||||||
/^[\w\s-]*$/.test(value) ||
|
|
||||||
`Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`,
|
|
||||||
}}
|
}}
|
||||||
placeholder="e.g. My Workspace"
|
onChange={(e) =>
|
||||||
className="placeholder:text-brand-secondary"
|
/^[a-zA-Z0-9_-]+$/.test(e.target.value)
|
||||||
error={errors.name}
|
? setInvalidSlug(false)
|
||||||
|
: setInvalidSlug(true)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
{slugError && (
|
||||||
<span className="text-sm">Workspace URL</span>
|
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
||||||
<div className="flex w-full items-center rounded-md border border-brand-base px-3">
|
)}
|
||||||
<span className="whitespace-nowrap text-sm text-brand-secondary">
|
{invalidSlug && (
|
||||||
{typeof window !== "undefined" && window.location.origin}/
|
<span className="text-sm text-red-500">{`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`}</span>
|
||||||
</span>
|
)}
|
||||||
<Input
|
|
||||||
mode="trueTransparent"
|
|
||||||
autoComplete="off"
|
|
||||||
name="slug"
|
|
||||||
register={register}
|
|
||||||
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
|
|
||||||
validations={{
|
|
||||||
required: "Workspace URL is required",
|
|
||||||
}}
|
|
||||||
onChange={(e) =>
|
|
||||||
/^[a-zA-Z0-9_-]+$/.test(e.target.value)
|
|
||||||
? setInvalidSlug(false)
|
|
||||||
: setInvalidSlug(true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{slugError && (
|
|
||||||
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
|
||||||
)}
|
|
||||||
{invalidSlug && (
|
|
||||||
<span className="text-sm text-red-500">{`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-brand-base px-4 py-7">
|
|
||||||
<span className="text-sm">How large is your company?</span>
|
|
||||||
<div className="w-full">
|
|
||||||
<Controller
|
|
||||||
name="company_size"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: "This field is required" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
value ? (
|
|
||||||
value.toString()
|
|
||||||
) : (
|
|
||||||
<span className="text-brand-secondary">Select company size</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
input
|
|
||||||
width="w-full"
|
|
||||||
>
|
|
||||||
{COMPANY_SIZE?.map((item) => (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.company_size && (
|
|
||||||
<span className="text-sm text-red-500">{errors.company_size.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-center rounded-b-[10px] py-7">
|
|
||||||
<PrimaryButton
|
|
||||||
type="submit"
|
|
||||||
className="flex w-1/2 items-center justify-center text-center"
|
|
||||||
size="md"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Creating..." : "Create Workspace"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-brand-base px-7 pt-3.5 ">
|
||||||
|
<span className="text-sm">How large is your company?</span>
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
name="company_size"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: "This field is required" }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
value ? (
|
||||||
|
value.toString()
|
||||||
|
) : (
|
||||||
|
<span className="text-brand-secondary">Select company size</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
input
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
{COMPANY_SIZE?.map((item) => (
|
||||||
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.company_size && (
|
||||||
|
<span className="text-sm text-red-500">{errors.company_size.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full items-center justify-center rounded-b-[10px] pt-10">
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
className="flex w-1/2 items-center justify-center text-center"
|
||||||
|
size="md"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Creating..." : "Create Workspace"}
|
||||||
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -67,17 +67,19 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
router.push("/signin").then(() => {
|
await authenticationService
|
||||||
mutateUser();
|
.signOut()
|
||||||
});
|
.then(() => {
|
||||||
|
mutateUser(undefined);
|
||||||
await authenticationService.signOut().catch(() =>
|
router.push("/");
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Failed to sign out. Please try again.",
|
|
||||||
})
|
})
|
||||||
);
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Failed to sign out. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
border border-brand-base bg-brand-surface-2 shadow-lg focus:outline-none"
|
border border-brand-base bg-brand-surface-2 shadow-lg focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
||||||
<div className="text-sm text-brand-secondary">{user?.email}</div>
|
<div className="text-sm text-gray-500">{user?.email}</div>
|
||||||
<span className="text-sm font-semibold text-brand-secondary">Workspace</span>
|
<span className="text-sm font-semibold text-gray-500">Workspace</span>
|
||||||
{workspaces ? (
|
{workspaces ? (
|
||||||
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
|
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
|
||||||
{workspaces.length > 0 ? (
|
{workspaces.length > 0 ? (
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// next
|
// next
|
||||||
|
import { getFirstCharacters, truncateText } from "helpers/string.helper";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// react
|
// react
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -22,9 +23,7 @@ const SingleInvitation: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
<label
|
<label
|
||||||
className={`group relative flex cursor-pointer items-start space-x-3 border-2 border-transparent px-4 py-4 ${
|
className={`group relative flex cursor-pointer items-start space-x-3 border-2 border-transparent py-4`}
|
||||||
isChecked ? "rounded-lg border-theme" : ""
|
|
||||||
}`}
|
|
||||||
htmlFor={invitation.id}
|
htmlFor={invitation.id}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@ -38,35 +37,36 @@ const SingleInvitation: React.FC<Props> = ({
|
|||||||
alt={invitation.workspace.name}
|
alt={invitation.workspace.name}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-full w-full items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
<span className="flex h-full w-full items-center justify-center rounded-xl bg-gray-700 p-4 uppercase text-white">
|
||||||
{invitation.workspace.name.charAt(0)}
|
{getFirstCharacters(invitation.workspace.name)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium text-brand-base">{invitation.workspace.name}</div>
|
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
|
||||||
<p className="text-sm text-brand-secondary">
|
<p className="text-sm text-brand-secondary">
|
||||||
Invited by {invitation.workspace.owner.first_name}
|
Invited by {invitation.workspace.owner.first_name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<input
|
<button
|
||||||
id={invitation.id}
|
className={`${
|
||||||
aria-describedby="workspaces"
|
invitationsRespond.includes(invitation.id)
|
||||||
name={invitation.id}
|
? "bg-brand-surface-2 text-brand-secondary"
|
||||||
checked={invitationsRespond.includes(invitation.id)}
|
: "bg-brand-accent text-white"
|
||||||
value={invitation.workspace.name}
|
} text-sm px-4 py-2 border border-brand-base rounded-3xl`}
|
||||||
onChange={(e) => {
|
onClick={(e) => {
|
||||||
handleInvitation(
|
handleInvitation(
|
||||||
invitation,
|
invitation,
|
||||||
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
|
||||||
);
|
);
|
||||||
setIsChecked(e.target.checked);
|
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
>
|
||||||
className="h-4 w-4 rounded border-brand-base text-brand-accent focus:ring-indigo-500"
|
{invitationsRespond.includes(invitation.id)
|
||||||
/>
|
? "Invitation Accepted"
|
||||||
|
: "Accept Invitation"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
@ -109,3 +109,12 @@ export const generateRandomColor = (string: string): string => {
|
|||||||
|
|
||||||
return randomColor;
|
return randomColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFirstCharacters = (str: string) => {
|
||||||
|
const words = str.trim().split(" ");
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].charAt(0);
|
||||||
|
} else {
|
||||||
|
return words[0].charAt(0) + words[1].charAt(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
107
apps/app/hooks/use-user-auth.tsx
Normal file
107
apps/app/hooks/use-user-auth.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
// next imports
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// keys
|
||||||
|
import { CURRENT_USER } from "constants/fetch-keys";
|
||||||
|
// services
|
||||||
|
import userService from "services/user.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// types
|
||||||
|
import type { IWorkspace, ICurrentUserResponse } from "types";
|
||||||
|
|
||||||
|
const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isRouteAccess, setIsRouteAccess] = useState(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: user,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
} = useSWR<ICurrentUserResponse>(CURRENT_USER, () => userService.currentUser());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleWorkSpaceRedirection = async () => {
|
||||||
|
workspaceService.userWorkspaces().then(async (userWorkspaces) => {
|
||||||
|
const lastActiveWorkspace = userWorkspaces.find(
|
||||||
|
(workspace: IWorkspace) => workspace.id === user?.last_workspace_id
|
||||||
|
);
|
||||||
|
if (lastActiveWorkspace) {
|
||||||
|
router.push(`/${lastActiveWorkspace.slug}`);
|
||||||
|
return;
|
||||||
|
} else if (userWorkspaces.length > 0) {
|
||||||
|
router.push(`/${userWorkspaces[0].slug}`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const invitations = await workspaceService.userWorkspaceInvitations();
|
||||||
|
if (invitations.length > 0) {
|
||||||
|
router.push(`/invitations`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
router.push(`/create-workspace`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserRouteAuthentication = async () => {
|
||||||
|
console.log("user", user);
|
||||||
|
|
||||||
|
if (user && user.is_active) {
|
||||||
|
if (routeAuth === "sign-in") {
|
||||||
|
if (user.is_onboarded) handleWorkSpaceRedirection();
|
||||||
|
else {
|
||||||
|
router.push("/onboarding");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (routeAuth === "onboarding") {
|
||||||
|
if (user.is_onboarded) handleWorkSpaceRedirection();
|
||||||
|
else {
|
||||||
|
setIsRouteAccess(() => false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!user.is_onboarded) {
|
||||||
|
router.push("/onboarding");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setIsRouteAccess(() => false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// user is not active and we can redirect to no access page
|
||||||
|
// router.push("/no-access");
|
||||||
|
// remove token
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isLoading) {
|
||||||
|
setIsRouteAccess(() => true);
|
||||||
|
if (user) handleUserRouteAuthentication();
|
||||||
|
else {
|
||||||
|
if (routeAuth === "sign-in") {
|
||||||
|
setIsRouteAccess(() => false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [user, isLoading, routeAuth, router]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: isRouteAccess,
|
||||||
|
user: error ? undefined : user,
|
||||||
|
mutateUser: mutate,
|
||||||
|
assignedIssuesLength: user?.assigned_issues ?? 0,
|
||||||
|
workspaceInvitesLength: user?.workspace_invites ?? 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserAuth;
|
@ -1,13 +1,55 @@
|
|||||||
import { useContext } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import useSWR from "swr";
|
||||||
|
// services
|
||||||
|
import userService from "services/user.service";
|
||||||
|
// constants
|
||||||
|
import { CURRENT_USER } from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import type { ICurrentUserResponse, IUser } from "types";
|
||||||
|
|
||||||
// context
|
export default function useUser({ redirectTo = "", redirectIfFound = false, options = {} } = {}) {
|
||||||
import { UserContext } from "contexts/user.context";
|
const router = useRouter();
|
||||||
|
// API to fetch user information
|
||||||
|
const { data, isLoading, error, mutate } = useSWR<ICurrentUserResponse>(
|
||||||
|
CURRENT_USER,
|
||||||
|
() => userService.currentUser(),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
const useUser = () => {
|
const user = error ? undefined : data;
|
||||||
// context
|
// console.log("useUser", user);
|
||||||
const contextData = useContext(UserContext);
|
|
||||||
|
|
||||||
return { ...contextData };
|
useEffect(() => {
|
||||||
};
|
// if no redirect needed, just return (example: already on /dashboard)
|
||||||
|
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
|
||||||
|
if (!redirectTo || !user) return;
|
||||||
|
|
||||||
export default useUser;
|
if (
|
||||||
|
// If redirectTo is set, redirect if the user was not found.
|
||||||
|
(redirectTo && !redirectIfFound) ||
|
||||||
|
// If redirectIfFound is also set, redirect if the user was found
|
||||||
|
(redirectIfFound && user)
|
||||||
|
) {
|
||||||
|
router.push(redirectTo);
|
||||||
|
return;
|
||||||
|
// const nextLocation = router.asPath.split("?next=")[1];
|
||||||
|
// if (nextLocation) {
|
||||||
|
// router.push(nextLocation as string);
|
||||||
|
// return;
|
||||||
|
// } else {
|
||||||
|
// router.push("/");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}, [user, redirectIfFound, redirectTo, router]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
isUserLoading: isLoading,
|
||||||
|
mutateUser: mutate,
|
||||||
|
userError: error,
|
||||||
|
assignedIssuesLength: user?.assigned_issues ?? 0,
|
||||||
|
workspaceInvitesLength: user?.workspace_invites ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -32,7 +32,7 @@ export const UserAuthorizationLayout: React.FC<Props> = ({ children }) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
const redirectTo = router.asPath;
|
const redirectTo = router.asPath;
|
||||||
|
|
||||||
router.push(`/signin?next=${redirectTo}`);
|
router.push(`/?next=${redirectTo}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export const homePageRedirect = async (cookie?: string) => {
|
|||||||
if (!user)
|
if (!user)
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/signin",
|
destination: "/",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"recharts": "^2.3.2",
|
"recharts": "^2.3.2",
|
||||||
"remirror": "^2.0.23",
|
"remirror": "^2.0.23",
|
||||||
"swr": "^1.3.0",
|
"swr": "^2.1.3",
|
||||||
"tlds": "^1.238.0",
|
"tlds": "^1.238.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import fileService from "services/file.service";
|
import fileService from "services/file.service";
|
||||||
import userService from "services/user.service";
|
import userService from "services/user.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
@ -50,7 +50,7 @@ const Profile: NextPage = () => {
|
|||||||
} = useForm<IUser>({ defaultValues });
|
} = useForm<IUser>({ defaultValues });
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { user: myProfile, mutateUser } = useUser();
|
const { user: myProfile, mutateUser } = useUserAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset({ ...defaultValues, ...myProfile });
|
reset({ ...defaultValues, ...myProfile });
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
import SettingsNavbar from "layouts/settings-navbar";
|
import SettingsNavbar from "layouts/settings-navbar";
|
||||||
@ -15,7 +15,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
import { ICustomTheme } from "types";
|
import { ICustomTheme } from "types";
|
||||||
|
|
||||||
const ProfilePreferences = () => {
|
const ProfilePreferences = () => {
|
||||||
const { user: myProfile } = useUser();
|
const { user: myProfile } = useUserAuth();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
|
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
|
||||||
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
|
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
|
||||||
|
@ -78,12 +78,15 @@ const IssueDetailsPage: NextPage = () => {
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData: IIssue) => ({
|
(prevData) => {
|
||||||
...prevData,
|
if (!prevData) return prevData;
|
||||||
...formData,
|
return {
|
||||||
}),
|
...prevData,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -44,31 +44,14 @@ Router.events.on("routeChangeComplete", NProgress.done);
|
|||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider themes={THEMES} defaultTheme="light">
|
// <UserProvider>
|
||||||
<UserProvider>
|
<ToastContextProvider>
|
||||||
<ToastContextProvider>
|
<ThemeContextProvider>
|
||||||
<ThemeContextProvider>
|
<CrispWithNoSSR />
|
||||||
<CrispWithNoSSR />{" "}
|
<Component {...pageProps} />
|
||||||
<Head>
|
</ThemeContextProvider>
|
||||||
<title>{SITE_TITLE}</title>
|
</ToastContextProvider>
|
||||||
<meta property="og:site_name" content={SITE_NAME} />
|
// </UserProvider>
|
||||||
<meta property="og:title" content={SITE_TITLE} />
|
|
||||||
<meta property="og:url" content={SITE_URL} />
|
|
||||||
<meta name="description" content={SITE_DESCRIPTION} />
|
|
||||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
|
||||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
|
||||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest.json" />
|
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</ThemeContextProvider>
|
|
||||||
</ToastContextProvider>
|
|
||||||
</UserProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const CustomErrorComponent = () => {
|
|||||||
message: "Failed to sign out. Please try again.",
|
message: "Failed to sign out. Please try again.",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.finally(() => router.push("/signin"));
|
.finally(() => router.push("/"));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
// components
|
||||||
|
import { OnboardingLogo } from "components/onboarding";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
||||||
@ -21,19 +24,36 @@ const CreateWorkspace: NextPage = () => {
|
|||||||
company_size: null,
|
company_size: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
return (
|
return (
|
||||||
<UserAuthorizationLayout>
|
<UserAuthorizationLayout>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="relative grid h-full place-items-center p-5">
|
||||||
<div className="w-full space-y-4">
|
<div className="h-full flex flex-col items-center justify-center w-full py-4">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-7 flex items-center justify-center text-center">
|
||||||
<Image src={Logo} height="50" alt="Plane Logo" />
|
<OnboardingLogo className="h-12 w-48 fill-current text-brand-base" />
|
||||||
</div>
|
</div>
|
||||||
<CreateWorkspaceForm
|
|
||||||
defaultValues={defaultValues}
|
<div className="flex h-[366px] w-full max-w-xl flex-col justify-between rounded-[10px] bg-brand-base shadow-md">
|
||||||
setDefaultValues={() => {}}
|
<div className="flex items-center justify-start gap-3 px-7 pt-7 pb-3.5 text-gray-8 text-sm">
|
||||||
onSubmit={(res) => router.push(`/${res.slug}`)}
|
<div className="flex flex-col gap-2 justify-center ">
|
||||||
/>
|
<h3 className="text-base font-semibold text-brand-base">Create Workspace</h3>
|
||||||
|
<p className="text-sm text-brand-secondary">
|
||||||
|
Create or join the workspace to get started with Plane.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CreateWorkspaceForm
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
setDefaultValues={() => {}}
|
||||||
|
onSubmit={(res) => router.push(`/${res.slug}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
|
||||||
|
<span className="text-xs text-brand-secondary">Logged in:</span>
|
||||||
|
<span className="text-sm text-brand-base">{user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
@ -1,66 +1,116 @@
|
|||||||
import { useEffect } from "react";
|
import React from "react";
|
||||||
|
// next imports
|
||||||
import Router from "next/router";
|
import Image from "next/image";
|
||||||
|
// next types
|
||||||
|
import type { NextPage } from "next";
|
||||||
|
// layouts
|
||||||
|
import DefaultLayout from "layouts/default-layout";
|
||||||
|
// hooks
|
||||||
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import userService from "services/user.service";
|
import authenticationService from "services/authentication.service";
|
||||||
import workspaceService from "services/workspace.service";
|
// social auth buttons
|
||||||
|
import {
|
||||||
|
GoogleLoginButton,
|
||||||
|
GithubLoginButton,
|
||||||
|
EmailCodeForm,
|
||||||
|
EmailPasswordForm,
|
||||||
|
} from "components/account";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "components/ui";
|
||||||
// types
|
// icons
|
||||||
import type { NextPage } from "next";
|
import Logo from "public/logo.png";
|
||||||
|
|
||||||
const redirectUserTo = async () => {
|
const HomePage: NextPage = () => {
|
||||||
const user = await userService
|
const { user, isLoading, mutateUser } = useUserAuth("sign-in");
|
||||||
.currentUser()
|
|
||||||
.then((res) => res)
|
|
||||||
.catch(() => {
|
|
||||||
Router.push("/signin");
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
const { setToastAlert } = useToast();
|
||||||
Router.push("/signin");
|
|
||||||
return;
|
|
||||||
} else if (!user.user.is_onboarded) {
|
|
||||||
Router.push("/onboarding");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const userWorkspaces = await workspaceService.userWorkspaces();
|
|
||||||
|
|
||||||
const lastActiveWorkspace = userWorkspaces.find(
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
(workspace) => workspace.id === user.user.last_workspace_id
|
try {
|
||||||
);
|
if (clientId && credential) {
|
||||||
|
mutateUser(
|
||||||
if (lastActiveWorkspace) {
|
await authenticationService.socialAuth({
|
||||||
Router.push(`/${lastActiveWorkspace.slug}`);
|
medium: "google",
|
||||||
return;
|
credential,
|
||||||
} else if (userWorkspaces.length > 0) {
|
clientId,
|
||||||
Router.push(`/${userWorkspaces[0].slug}`);
|
})
|
||||||
return;
|
);
|
||||||
} else {
|
|
||||||
const invitations = await workspaceService.userWorkspaceInvitations();
|
|
||||||
if (invitations.length > 0) {
|
|
||||||
Router.push(`/invitations`);
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
Router.push(`/create-workspace`);
|
throw Error("Cant find credentials");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error signing in!",
|
||||||
|
type: "error",
|
||||||
|
message: "Something went wrong. Please try again later or contact the support team.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const handleGithubSignIn = async (credential: string) => {
|
||||||
useEffect(() => {
|
try {
|
||||||
redirectUserTo();
|
if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) {
|
||||||
}, []);
|
mutateUser(
|
||||||
|
await authenticationService.socialAuth({
|
||||||
|
medium: "github",
|
||||||
|
credential,
|
||||||
|
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Error("Cant find credentials");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error signing in!",
|
||||||
|
type: "error",
|
||||||
|
message: "Something went wrong. Please try again later or contact the support team.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-screen place-items-center">
|
<DefaultLayout>
|
||||||
<Spinner />
|
{isLoading ? (
|
||||||
</div>
|
<div className="grid h-screen place-items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-screen w-full items-center justify-center overflow-auto bg-gray-50">
|
||||||
|
<div className="flex min-h-full w-full flex-col justify-center py-12 px-6 lg:px-8">
|
||||||
|
<div className="flex flex-col gap-10 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10">
|
||||||
|
<Image src={Logo} height={80} width={80} alt="Plane Web Logo" />
|
||||||
|
<div className="text-center text-xl font-medium text-black">
|
||||||
|
Sign In to your Plane Account
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col rounded-[10px] bg-white shadow-md">
|
||||||
|
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
|
||||||
|
<>
|
||||||
|
<EmailCodeForm />
|
||||||
|
<div className="flex flex-col gap-3 py-5 px-5 border-t items-center justify-center border-gray-300 ">
|
||||||
|
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
|
||||||
|
<GithubLoginButton handleSignIn={handleGithubSignIn} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EmailPasswordForm />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DefaultLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export default HomePage;
|
||||||
|
@ -15,6 +15,7 @@ import DefaultLayout from "layouts/default-layout";
|
|||||||
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
||||||
// components
|
// components
|
||||||
import SingleInvitation from "components/workspace/single-invitation";
|
import SingleInvitation from "components/workspace/single-invitation";
|
||||||
|
import { OnboardingLogo } from "components/onboarding";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, EmptySpace, EmptySpaceItem, SecondaryButton, PrimaryButton } from "components/ui";
|
import { Spinner, EmptySpace, EmptySpaceItem, SecondaryButton, PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -81,86 +82,93 @@ const OnBoard: NextPage = () => {
|
|||||||
return (
|
return (
|
||||||
<UserAuthorizationLayout>
|
<UserAuthorizationLayout>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="flex min-h-full flex-col items-center justify-center p-4 sm:p-0">
|
<div className="relative grid h-full place-items-center p-5">
|
||||||
{user && (
|
<div className="h-full flex flex-col items-center justify-center w-full py-4">
|
||||||
<div className="mb-10 w-96 rounded-lg bg-brand-accent/20 p-2 text-brand-accent">
|
<div className="mb-7 flex items-center justify-center text-center">
|
||||||
<p className="text-center text-sm">logged in as {user.email}</p>
|
<OnboardingLogo className="h-12 w-48 fill-current text-brand-base" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-full rounded-lg p-8 md:w-2/3 lg:w-1/3">
|
<div className="flex h-[436px] w-full max-w-xl rounded-[10px] p-7 bg-brand-base shadow-md">
|
||||||
{invitations && workspaces ? (
|
{invitations && workspaces ? (
|
||||||
invitations.length > 0 ? (
|
invitations.length > 0 ? (
|
||||||
<div>
|
<div className="flex w-full flex-col gap-3 justify-between">
|
||||||
<h2 className="text-lg font-medium">Workspace Invitations</h2>
|
<div className="flex flex-col gap-2 justify-center ">
|
||||||
<p className="mt-1 text-sm text-brand-secondary">
|
<h3 className="text-base font-semibold text-brand-base">
|
||||||
Select invites that you want to accept.
|
Workspace Invitations
|
||||||
</p>
|
</h3>
|
||||||
<ul
|
<p className="text-sm text-brand-secondary">
|
||||||
role="list"
|
Create or join the workspace to get started with Plane.
|
||||||
className="mt-6 divide-y divide-brand-base border-t border-b border-brand-base"
|
</p>
|
||||||
>
|
</div>
|
||||||
{invitations.map((invitation) => (
|
|
||||||
<SingleInvitation
|
<ul role="list" className="h-[255px] w-full overflow-y-auto">
|
||||||
key={invitation.id}
|
{invitations.map((invitation) => (
|
||||||
invitation={invitation}
|
<SingleInvitation
|
||||||
invitationsRespond={invitationsRespond}
|
key={invitation.id}
|
||||||
handleInvitation={handleInvitation}
|
invitation={invitation}
|
||||||
/>
|
invitationsRespond={invitationsRespond}
|
||||||
))}
|
handleInvitation={handleInvitation}
|
||||||
</ul>
|
/>
|
||||||
<div className="mt-6 flex items-center gap-2">
|
))}
|
||||||
<Link href="/">
|
</ul>
|
||||||
<a className="w-full">
|
|
||||||
<SecondaryButton className="w-full">Go Home</SecondaryButton>
|
<div className="flex items-center gap-3">
|
||||||
</a>
|
<Link href="/">
|
||||||
</Link>
|
<a className="w-full">
|
||||||
<PrimaryButton className="w-full" onClick={submitInvitations}>
|
<SecondaryButton className="w-full">Go Home</SecondaryButton>
|
||||||
Accept and Continue
|
</a>
|
||||||
</PrimaryButton>
|
</Link>
|
||||||
|
<PrimaryButton className="w-full" onClick={submitInvitations}>
|
||||||
|
Accept and Continue
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : workspaces && workspaces.length > 0 ? (
|
||||||
) : workspaces && workspaces.length > 0 ? (
|
<div className="flex flex-col gap-y-3">
|
||||||
<div className="flex flex-col gap-y-3">
|
<h2 className="mb-4 text-xl font-medium">Your workspaces</h2>
|
||||||
<h2 className="mb-4 text-xl font-medium">Your workspaces</h2>
|
{workspaces.map((workspace) => (
|
||||||
{workspaces.map((workspace) => (
|
<Link key={workspace.id} href={workspace.slug}>
|
||||||
<Link key={workspace.id} href={workspace.slug}>
|
<a>
|
||||||
<a>
|
<div className="mb-2 flex items-center justify-between rounded border border-brand-base px-4 py-2">
|
||||||
<div className="mb-2 flex items-center justify-between rounded border border-brand-base px-4 py-2">
|
<div className="flex items-center gap-x-2 text-sm">
|
||||||
<div className="flex items-center gap-x-2 text-sm">
|
<CubeIcon className="h-5 w-5 text-brand-secondary" />
|
||||||
<CubeIcon className="h-5 w-5 text-brand-secondary" />
|
{workspace.name}
|
||||||
{workspace.name}
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2 text-xs text-brand-secondary">
|
||||||
|
{workspace.owner.first_name}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2 text-xs text-brand-secondary">
|
</a>
|
||||||
{workspace.owner.first_name}
|
</Link>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
) : (
|
||||||
</Link>
|
invitations.length === 0 &&
|
||||||
))}
|
workspaces.length === 0 && (
|
||||||
</div>
|
<EmptySpace
|
||||||
) : (
|
title="You don't have any workspaces yet"
|
||||||
invitations.length === 0 &&
|
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
|
||||||
workspaces.length === 0 && (
|
>
|
||||||
<EmptySpace
|
<EmptySpaceItem
|
||||||
title="You don't have any workspaces yet"
|
Icon={PlusIcon}
|
||||||
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
|
title={"Create your Workspace"}
|
||||||
>
|
action={() => {
|
||||||
<EmptySpaceItem
|
router.push("/create-workspace");
|
||||||
Icon={PlusIcon}
|
}}
|
||||||
title={"Create your Workspace"}
|
/>
|
||||||
action={() => {
|
</EmptySpace>
|
||||||
router.push("/create-workspace");
|
)
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EmptySpace>
|
|
||||||
)
|
)
|
||||||
)
|
) : (
|
||||||
) : (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<Spinner />
|
||||||
<Spinner />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
|
||||||
|
<span className="text-xs text-brand-secondary">Logged in:</span>
|
||||||
|
<span className="text-sm text-brand-base">{user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import Router, { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import userService from "services/user.service";
|
import userService from "services/user.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
||||||
@ -38,15 +39,17 @@ const Onboarding: NextPage = () => {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUserAuth("onboarding");
|
||||||
|
|
||||||
|
console.log("user", user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserAuthorizationLayout>
|
<UserAuthorizationLayout>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="relative grid h-full place-items-center p-5">
|
||||||
{step <= 3 ? (
|
{step <= 3 ? (
|
||||||
<div className="w-full">
|
<div className="h-full flex flex-col justify-center w-full py-4">
|
||||||
<div className="mb-8 flex items-center justify-center text-center">
|
<div className="mb-7 flex items-center justify-center text-center">
|
||||||
<OnboardingLogo className="h-12 w-48 fill-current text-brand-base" />
|
<OnboardingLogo className="h-12 w-48 fill-current text-brand-base" />
|
||||||
</div>
|
</div>
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
@ -59,7 +62,7 @@ const Onboarding: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full max-w-2xl flex-col gap-12">
|
<div className="flex w-full max-w-2xl flex-col gap-12">
|
||||||
<div className="flex flex-col items-center justify-center gap-7 rounded-[10px] bg-brand-base pb-10 text-center shadow-md">
|
<div className="flex flex-col items-center justify-center gap-7 rounded-[10px] bg-brand-base pb-7 text-center shadow-md">
|
||||||
{step === 4 ? (
|
{step === 4 ? (
|
||||||
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
|
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
|
||||||
) : step === 5 ? (
|
) : step === 5 ? (
|
||||||
@ -80,7 +83,7 @@ const Onboarding: NextPage = () => {
|
|||||||
if (step === 8) {
|
if (step === 8) {
|
||||||
userService
|
userService
|
||||||
.updateUserOnBoard({ userRole })
|
.updateUserOnBoard({ userRole })
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
mutate<ICurrentUserResponse>(
|
mutate<ICurrentUserResponse>(
|
||||||
CURRENT_USER,
|
CURRENT_USER,
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
@ -96,7 +99,28 @@ const Onboarding: NextPage = () => {
|
|||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
router.push("/");
|
const userWorkspaces = await workspaceService.userWorkspaces();
|
||||||
|
|
||||||
|
const lastActiveWorkspace = userWorkspaces.find(
|
||||||
|
(workspace) => workspace.id === user?.last_workspace_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastActiveWorkspace) {
|
||||||
|
Router.push(`/${lastActiveWorkspace.slug}`);
|
||||||
|
return;
|
||||||
|
} else if (userWorkspaces.length > 0) {
|
||||||
|
Router.push(`/${userWorkspaces[0].slug}`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const invitations = await workspaceService.userWorkspaceInvitations();
|
||||||
|
if (invitations.length > 0) {
|
||||||
|
Router.push(`/invitations`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Router.push(`/create-workspace`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@ -110,6 +134,10 @@ const Onboarding: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
|
||||||
|
<span className="text-xs text-brand-secondary">Logged in:</span>
|
||||||
|
<span className="text-sm text-brand-base">{user?.email}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
</UserAuthorizationLayout>
|
</UserAuthorizationLayout>
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Image from "next/image";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// services
|
|
||||||
import authenticationService from "services/authentication.service";
|
|
||||||
// layouts
|
|
||||||
import DefaultLayout from "layouts/default-layout";
|
|
||||||
// social button
|
|
||||||
import {
|
|
||||||
GoogleLoginButton,
|
|
||||||
GithubLoginButton,
|
|
||||||
EmailSignInForm,
|
|
||||||
EmailPasswordForm,
|
|
||||||
} from "components/account";
|
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import Logo from "public/logo.png";
|
|
||||||
// types
|
|
||||||
import type { NextPage } from "next";
|
|
||||||
|
|
||||||
const SignInPage: NextPage = () => {
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// user hook
|
|
||||||
const { mutateUser } = useUser();
|
|
||||||
// states
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const onSignInSuccess = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
await mutateUser();
|
|
||||||
const nextLocation = router.asPath.split("?next=")[1];
|
|
||||||
if (nextLocation) await router.push(nextLocation as string);
|
|
||||||
else await router.push("/");
|
|
||||||
}, [mutateUser, router]);
|
|
||||||
|
|
||||||
const handleGoogleSignIn = ({ clientId, credential }: any) => {
|
|
||||||
if (clientId && credential) {
|
|
||||||
setLoading(true);
|
|
||||||
authenticationService
|
|
||||||
.socialAuth({
|
|
||||||
medium: "google",
|
|
||||||
credential,
|
|
||||||
clientId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await onSignInSuccess();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error signing in!",
|
|
||||||
type: "error",
|
|
||||||
message: "Something went wrong. Please try again later or contact the support team.",
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGithubSignIn = useCallback(
|
|
||||||
(credential: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
authenticationService
|
|
||||||
.socialAuth({
|
|
||||||
medium: "github",
|
|
||||||
credential,
|
|
||||||
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await onSignInSuccess();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error signing in!",
|
|
||||||
type: "error",
|
|
||||||
message: "Something went wrong. Please try again later or contact the support team.",
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[onSignInSuccess, setToastAlert]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DefaultLayout>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="absolute top-0 left-0 z-50 flex h-full w-full flex-col items-center justify-center gap-y-3">
|
|
||||||
<h2 className="text-xl text-brand-base">Signing in. Please wait...</h2>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-screen w-full items-center justify-center overflow-auto">
|
|
||||||
<div className="flex min-h-full w-full flex-col justify-center py-12 px-6 lg:px-8">
|
|
||||||
<div className="flex flex-col gap-10 sm:mx-auto sm:w-full sm:max-w-md">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-10">
|
|
||||||
<Image src={Logo} height={80} width={80} alt="Plane Web Logo" />
|
|
||||||
<h2 className="text-center text-xl font-medium text-brand-base">
|
|
||||||
Sign In to your Plane Account
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col rounded-[10px] bg-brand-base shadow-md">
|
|
||||||
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
|
|
||||||
<>
|
|
||||||
<EmailSignInForm handleSuccess={onSignInSuccess} />
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-center gap-3 border-t border-brand-base py-5 px-5 ">
|
|
||||||
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
|
|
||||||
|
|
||||||
<GithubLoginButton handleSignIn={handleGithubSignIn} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<EmailPasswordForm onSuccess={onSignInSuccess} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DefaultLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignInPage;
|
|
@ -49,7 +49,7 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
if (email === user?.email) {
|
if (email === user?.email) {
|
||||||
router.push("/invitations");
|
router.push("/invitations");
|
||||||
} else {
|
} else {
|
||||||
router.push("/signin");
|
router.push("/");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
@ -108,7 +108,7 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
Icon={UserIcon}
|
Icon={UserIcon}
|
||||||
title="Sign in to continue"
|
title="Sign in to continue"
|
||||||
action={() => {
|
action={() => {
|
||||||
router.push("/signin");
|
router.push("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -9,7 +9,8 @@ axios.interceptors.response.use(
|
|||||||
if (unAuthorizedStatus.includes(status)) {
|
if (unAuthorizedStatus.includes(status)) {
|
||||||
Cookies.remove("refreshToken", { path: "/" });
|
Cookies.remove("refreshToken", { path: "/" });
|
||||||
Cookies.remove("accessToken", { path: "/" });
|
Cookies.remove("accessToken", { path: "/" });
|
||||||
if (window.location.pathname != "/signin") window.location.href = "/signin";
|
if (window.location.pathname != "/")
|
||||||
|
window.location.href = "/?next_url=window.location.pathname";
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-bg-base: 243, 244, 246;
|
--color-bg-base: 255, 255, 255;
|
||||||
--color-bg-surface-1: 249, 250, 251;
|
--color-bg-surface-1: 249, 250, 251;
|
||||||
--color-bg-surface-2: 255, 255, 255;
|
--color-bg-surface-2: 243, 244, 246;
|
||||||
|
|
||||||
--color-border: 229, 231, 235;
|
--color-border: 229, 231, 235;
|
||||||
--color-bg-sidebar: 255, 255, 255;
|
--color-bg-sidebar: 255, 255, 255;
|
||||||
|
5
apps/app/types/users.d.ts
vendored
5
apps/app/types/users.d.ts
vendored
@ -41,10 +41,11 @@ export interface ICustomTheme {
|
|||||||
textSecondary: string;
|
textSecondary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICurrentUserResponse {
|
export interface ICurrentUserResponse extends IUser {
|
||||||
assigned_issues: number;
|
assigned_issues: number;
|
||||||
user: IUser;
|
// user: IUser;
|
||||||
workspace_invites: number;
|
workspace_invites: number;
|
||||||
|
is_onboarded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserLite {
|
export interface IUserLite {
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -8212,10 +8212,12 @@ svgmoji@^3.2.0:
|
|||||||
"@svgmoji/openmoji" "^3.2.0"
|
"@svgmoji/openmoji" "^3.2.0"
|
||||||
"@svgmoji/twemoji" "^3.2.0"
|
"@svgmoji/twemoji" "^3.2.0"
|
||||||
|
|
||||||
swr@^1.3.0:
|
swr@^2.1.3:
|
||||||
version "1.3.0"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
|
resolved "https://registry.yarnpkg.com/swr/-/swr-2.1.3.tgz#ae3608d4dc19f75121e0b51d1982735fbf02f52e"
|
||||||
integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
|
integrity sha512-g3ApxIM4Fjbd6vvEAlW60hJlKcYxHb+wtehogTygrh6Jsw7wNagv9m4Oj5Gq6zvvZw0tcyhVGL9L0oISvl3sUw==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
table@^6.0.9:
|
table@^6.0.9:
|
||||||
version "6.8.1"
|
version "6.8.1"
|
||||||
@ -8680,7 +8682,7 @@ use-sidecar@^1.1.2:
|
|||||||
detect-node-es "^1.1.0"
|
detect-node-es "^1.1.0"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-sync-external-store@1.2.0:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
Loading…
Reference in New Issue
Block a user