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
43 changed files with 821 additions and 593 deletions
|
|
@ -66,8 +66,12 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||
const handleSignin = async (formData: EmailCodeFormValues) => {
|
||||
await authenticationService
|
||||
.magicSignIn(formData)
|
||||
.then((response) => {
|
||||
onSuccess(response);
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Successfully logged in!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
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(() => {
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
setLoginCallBackURL(`${origin}/signin` as any);
|
||||
setLoginCallBackURL(`${origin}/` as any);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@ export * from "./google-login";
|
|||
export * from "./email-code-form";
|
||||
export * from "./email-password-form";
|
||||
export * from "./github-login-button";
|
||||
export * from "./email-signin-form";
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||
{user ? (
|
||||
<p>
|
||||
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>
|
||||
</Link>{" "}
|
||||
with different account that has access to this page.
|
||||
|
|
@ -47,7 +47,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||
) : (
|
||||
<p>
|
||||
You need to{" "}
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<Link href={`/?next=${currentPath}`}>
|
||||
<a className="font-medium text-brand-base">Sign in</a>
|
||||
</Link>{" "}
|
||||
with an account that has access to this page.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||
import Link from "next/link";
|
||||
// icons
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { Icon } from "components/ui";
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
children: any;
|
||||
|
|
@ -16,10 +17,13 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
|||
<div className="flex items-center">
|
||||
<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()}
|
||||
>
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
<Icon
|
||||
iconName="keyboard_backspace"
|
||||
className="text-base leading-4 text-brand-secondary group-hover:text-brand-base"
|
||||
/>
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,12 +57,15 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
|
|
@ -80,7 +83,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||
);
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
const updatedAssignees = issue.assignees ?? [];
|
||||
const updatedAssignees = issue.assignees_list ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) {
|
||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
|
|
|
|||
|
|
@ -27,12 +27,16 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }
|
|||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,15 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
|||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -120,12 +120,17 @@ export const CommandPalette: React.FC = () => {
|
|||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate(
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
(prevData: IIssue) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
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 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">
|
||||
<span>Email</span>
|
||||
<div className="w-full">
|
||||
<MultiInput
|
||||
name="emails"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type Props = {
|
|||
export const OnboardingCard: React.FC<Props> = ({ data, gradient = false }) => (
|
||||
<div
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -66,10 +66,17 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||
|
||||
return (
|
||||
<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 justify-between gap-3 px-10 py-7 sm:flex-row">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||
<div className="flex flex-col gap-2 justify-center px-7 pt-7 pb-3.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>
|
||||
<Input
|
||||
name="first_name"
|
||||
|
|
@ -81,7 +88,7 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||
error={errors.first_name}
|
||||
/>
|
||||
</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>
|
||||
<Input
|
||||
name="last_name"
|
||||
|
|
@ -94,7 +101,8 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||
/>
|
||||
</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>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
|
|
@ -123,6 +131,7 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-center ">
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
|||
import { CreateWorkspaceForm } from "components/workspace";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { getFirstCharacters, truncateText } from "helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
|
|
@ -30,6 +31,7 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||
slug: "",
|
||||
company_size: null,
|
||||
});
|
||||
const [currentTab, setCurrentTab] = useState("create");
|
||||
|
||||
const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||
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 (
|
||||
<div className="grid min-h-[490px] w-full place-items-center">
|
||||
<div className="grid w-full place-items-center">
|
||||
<Tab.Group
|
||||
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
|
||||
as="div"
|
||||
className="text-gray-8 flex items-center justify-start gap-3 px-4 pt-4 text-sm"
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-4 py-2 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
||||
}`
|
||||
}
|
||||
>
|
||||
New Workspace
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-5 py-2 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Invited Workspace
|
||||
</Tab>
|
||||
<Tab.List as="div" className="flex flex-col gap-3 px-7 pt-7 pb-3.5">
|
||||
<div className="flex flex-col gap-2 justify-center">
|
||||
<h3 className="text-base font-semibold text-brand-base">Workspaces</h3>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
Create or join the workspace to get started with Plane.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-gray-8 flex items-center justify-start gap-3 text-sm">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-4 py-2 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Invited Workspace
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-4 py-2 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
New Workspace
|
||||
</Tab>
|
||||
</div>
|
||||
</Tab.List>
|
||||
<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">
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className="divide-y px-4 py-7">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-[255px] overflow-y-auto px-7">
|
||||
{invitations && invitations.length > 0 ? (
|
||||
invitations.map((invitation) => (
|
||||
<div key={invitation.id}>
|
||||
|
|
@ -129,34 +150,62 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||
alt={invitation.workspace.name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{invitation.workspace.name.charAt(0)}
|
||||
<span className="flex h-full w-full items-center justify-center rounded-xl bg-gray-700 p-4 uppercase text-white">
|
||||
{getFirstCharacters(invitation.workspace.name)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
Invited by {invitation.workspace.owner.first_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<input
|
||||
id={invitation.id}
|
||||
aria-describedby="workspaces"
|
||||
name={invitation.id}
|
||||
checked={invitationsRespond.includes(invitation.id)}
|
||||
value={invitation.workspace.name}
|
||||
onChange={(e) => {
|
||||
<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`}
|
||||
onClick={(e) => {
|
||||
handleInvitation(
|
||||
invitation,
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -167,7 +216,7 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||
</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
|
||||
type="submit"
|
||||
className="w-1/2 text-center"
|
||||
|
|
@ -180,6 +229,16 @@ export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="h-full">
|
||||
<CreateWorkspaceForm
|
||||
onSubmit={(res) => {
|
||||
setWorkspace(res);
|
||||
setStep(3);
|
||||
}}
|
||||
defaultValues={defaultValues}
|
||||
setDefaultValues={setDefaultValues}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -73,9 +73,12 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
|||
.inviteProject(workspaceSlug as string, projectId as string, formData)
|
||||
.then((response) => {
|
||||
setIsOpen(false);
|
||||
mutate(
|
||||
mutate<any[]>(
|
||||
PROJECT_INVITATIONS,
|
||||
(prevData: any[]) => [{ ...formData, ...response }, ...(prevData ?? [])],
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return [{ ...formData, ...response }, ...(prevData ?? [])];
|
||||
},
|
||||
false
|
||||
);
|
||||
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 "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
export * from "./icon";
|
||||
|
|
|
|||
|
|
@ -99,110 +99,105 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex w-full items-center justify-center"
|
||||
onSubmit={handleSubmit(handleCreateWorkspace)}
|
||||
>
|
||||
<div className="flex w-full max-w-xl flex-col">
|
||||
<div className="flex flex-col rounded-[10px] bg-brand-base">
|
||||
<div className="flex flex-col justify-between gap-3 px-4 py-7">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||
<span className="text-sm">Workspace name</span>
|
||||
<form className="flex h-full w-full flex-col" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||
<div className="divide-y h-[255px]">
|
||||
<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">
|
||||
<span className="text-sm">Workspace name</span>
|
||||
<Input
|
||||
name="name"
|
||||
register={register}
|
||||
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
|
||||
name="name"
|
||||
register={register}
|
||||
mode="trueTransparent"
|
||||
autoComplete="off"
|
||||
onChange={(e) =>
|
||||
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
|
||||
}
|
||||
name="slug"
|
||||
register={register}
|
||||
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
|
||||
validations={{
|
||||
required: "Workspace name is required",
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) ||
|
||||
`Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`,
|
||||
required: "Workspace URL is required",
|
||||
}}
|
||||
placeholder="e.g. My Workspace"
|
||||
className="placeholder:text-brand-secondary"
|
||||
error={errors.name}
|
||||
onChange={(e) =>
|
||||
/^[a-zA-Z0-9_-]+$/.test(e.target.value)
|
||||
? setInvalidSlug(false)
|
||||
: setInvalidSlug(true)
|
||||
}
|
||||
/>
|
||||
</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
|
||||
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>
|
||||
{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-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>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -67,17 +67,19 @@ export const WorkspaceSidebarDropdown = () => {
|
|||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
router.push("/signin").then(() => {
|
||||
mutateUser();
|
||||
});
|
||||
|
||||
await authenticationService.signOut().catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
await authenticationService
|
||||
.signOut()
|
||||
.then(() => {
|
||||
mutateUser(undefined);
|
||||
router.push("/");
|
||||
})
|
||||
);
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -137,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => {
|
|||
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="text-sm text-brand-secondary">{user?.email}</div>
|
||||
<span className="text-sm font-semibold text-brand-secondary">Workspace</span>
|
||||
<div className="text-sm text-gray-500">{user?.email}</div>
|
||||
<span className="text-sm font-semibold text-gray-500">Workspace</span>
|
||||
{workspaces ? (
|
||||
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
|
||||
{workspaces.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// next
|
||||
import { getFirstCharacters, truncateText } from "helpers/string.helper";
|
||||
import Image from "next/image";
|
||||
// react
|
||||
import { useState } from "react";
|
||||
|
|
@ -22,9 +23,7 @@ const SingleInvitation: React.FC<Props> = ({
|
|||
<>
|
||||
<li>
|
||||
<label
|
||||
className={`group relative flex cursor-pointer items-start space-x-3 border-2 border-transparent px-4 py-4 ${
|
||||
isChecked ? "rounded-lg border-theme" : ""
|
||||
}`}
|
||||
className={`group relative flex cursor-pointer items-start space-x-3 border-2 border-transparent py-4`}
|
||||
htmlFor={invitation.id}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
|
|
@ -38,35 +37,36 @@ const SingleInvitation: React.FC<Props> = ({
|
|||
alt={invitation.workspace.name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{invitation.workspace.name.charAt(0)}
|
||||
<span className="flex h-full w-full items-center justify-center rounded-xl bg-gray-700 p-4 uppercase text-white">
|
||||
{getFirstCharacters(invitation.workspace.name)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
Invited by {invitation.workspace.owner.first_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<input
|
||||
id={invitation.id}
|
||||
aria-describedby="workspaces"
|
||||
name={invitation.id}
|
||||
checked={invitationsRespond.includes(invitation.id)}
|
||||
value={invitation.workspace.name}
|
||||
onChange={(e) => {
|
||||
<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`}
|
||||
onClick={(e) => {
|
||||
handleInvitation(
|
||||
invitation,
|
||||
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>
|
||||
</label>
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue