chore: user auth layer (#749)

* chore: use estimate points hook created

* chore: user auth layer

* fix: build error
This commit is contained in:
Aaryan Khandelwal 2023-04-08 18:05:54 +05:30 committed by GitHub
parent 3fe32606a9
commit 1026ae3eb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 603 additions and 487 deletions

View File

@ -21,9 +21,10 @@ import useToast from "hooks/use-toast";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
} from "components/issues";
// ui
import { ContextMenu, CustomMenu } from "components/ui";
// icons
@ -48,6 +49,7 @@ import {
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
import useEstimateOption from "hooks/use-estimate-option";
type Props = {
type?: string;
@ -90,6 +92,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
const { orderBy, params } = useIssuesView();
const { estimateValue } = useEstimateOption(issue.estimate_point);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -342,6 +346,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
</div>
</div>
</div>

View File

@ -1,44 +1,41 @@
import React, { useState } from "react";
// ui
import { CustomMenu, PrimaryButton } from "components/ui";
// types
import { IEstimate, IProject } from "types";
//icons
import { PencilIcon, TrashIcon, SquaresPlusIcon, ListBulletIcon } from "@heroicons/react/24/outline";
import useSWR, { mutate } from "swr";
import useToast from "hooks/use-toast";
import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
import { EstimatePointsModal } from "./estimate-points-modal";
import { useRouter } from "next/router";
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
import { PlusIcon } from "components/icons";
import useSWR from "swr";
interface IEstimatePoints {
key: string;
value: string;
}
// services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
// components
import { EstimatePointsModal } from "components/estimates";
// ui
import { CustomMenu } from "components/ui";
//icons
import {
PencilIcon,
TrashIcon,
SquaresPlusIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
// types
import { IEstimate, IProject } from "types";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = {
estimate: IEstimate;
editEstimate: (estimate: IEstimate) => void;
handleEstimateDelete: (estimateId: string) => void;
activeEstimate: IEstimate | null;
setActiveEstimate: React.Dispatch<React.SetStateAction<IEstimate | null>>;
};
export const SingleEstimate: React.FC<Props> = ({
estimate,
editEstimate,
handleEstimateDelete,
activeEstimate,
setActiveEstimate,
}) => {
const [isEstimatePointsModalOpen, setIsEstimatePointsModalOpen] = useState(false);
@ -47,6 +44,8 @@ export const SingleEstimate: React.FC<Props> = ({
const { setToastAlert } = useToast();
const { projectDetails, mutateProjectDetails } = useProjectDetails();
const { data: estimatePoints } = useSWR(
workspaceSlug && projectId ? ESTIMATE_POINTS_LIST(estimate.id) : null,
workspaceSlug && projectId
@ -59,12 +58,19 @@ export const SingleEstimate: React.FC<Props> = ({
: null
);
const handleActiveEstimate = async () => {
if (!workspaceSlug || !projectId || !estimate) return;
const payload: Partial<IProject> = {
const handleUseEstimate = async () => {
if (!workspaceSlug || !projectId) return;
const payload = {
estimate: estimate.id,
};
setActiveEstimate(estimate);
mutateProjectDetails((prevData) => {
if (!prevData) return prevData;
return { ...prevData, estimate: estimate.id };
}, false);
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload)
.catch(() => {
@ -77,56 +83,63 @@ export const SingleEstimate: React.FC<Props> = ({
};
return (
<div className="divide-y">
<>
<EstimatePointsModal
isOpen={isEstimatePointsModalOpen}
estimate={estimate}
onClose={() => setIsEstimatePointsModalOpen(false)}
/>
<div className="gap-2 space-y-3 my-3 bg-white">
<div className="gap-2 py-3">
<div className="flex justify-between items-center">
<div className="items-start">
<h6 className="font-medium text-base w-[40vw] truncate">{estimate.name}</h6>
<div>
<h6 className="flex items-center gap-2 font-medium text-base w-[40vw] truncate">
{estimate.name}
{projectDetails?.estimate && projectDetails?.estimate === estimate.id && (
<span className="capitalize px-2 py-0.5 text-xs rounded bg-green-100 text-green-500">
In use
</span>
)}
</h6>
<p className="font-sm text-gray-400 font-normal text-[14px] w-[40vw] truncate">
{estimate.description}
</p>
</div>
<div className="flex items-center gap-8">
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={handleActiveEstimate}>
<span className="flex items-center justify-start gap-2">
<SquaresPlusIcon className="h-4 w-4" />
<CustomMenu ellipsis>
{projectDetails?.estimate && projectDetails?.estimate !== estimate.id && (
<CustomMenu.MenuItem onClick={handleUseEstimate}>
<div className="flex items-center justify-start gap-2">
<SquaresPlusIcon className="h-3.5 w-3.5" />
<span>Use estimate</span>
</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}>
<span className="flex items-center justify-start gap-2">
<ListBulletIcon className="h-4 w-4" />
{estimatePoints?.length === 8 ? "Update points" : "Create points"}
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
editEstimate(estimate);
}}
>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit estimate</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleEstimateDelete(estimate.id);
}}
>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete estimate</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
)}
<CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}>
<div className="flex items-center justify-start gap-2">
<ListBulletIcon className="h-3.5 w-3.5" />
<span>{estimatePoints?.length === 8 ? "Update points" : "Create points"}</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
editEstimate(estimate);
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit estimate</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleEstimateDelete(estimate.id);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
{estimatePoints && estimatePoints.length > 0 ? (
<div className="flex gap-2">
@ -140,11 +153,11 @@ export const SingleEstimate: React.FC<Props> = ({
{estimatePoints.length > 0 && ")"}
</div>
) : (
<div>
<p className= " text-sm text-gray-300">No estimate points</p>
</div>
<div>
<p className=" text-sm text-gray-300">No estimate points</p>
</div>
)}
</div>
</div>
</>
);
};

View File

@ -1,5 +1,6 @@
export * from "./comment";
export * from "./sidebar-select";
export * from "./view-select";
export * from "./activity";
export * from "./delete-issue-modal";
export * from "./description-form";

View File

@ -0,0 +1,69 @@
import React from "react";
// ui
import { CustomSelect, Tooltip } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
// types
import { IIssue } from "types";
// constants
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
import useEstimateOption from "hooks/use-estimate-option";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
};
export const ViewEstimateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "left",
selfPositioned = false,
isNotAllowed,
}) => {
const { isEstimateActive, estimatePoints, estimateValue } = useEstimateOption(
issue.estimate_point
);
return (
<CustomSelect
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date });
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY"
);
}}
label={
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
<>{estimateValue}</>
</Tooltip>
}
maxHeight="md"
noChevron
disabled={isNotAllowed}
position={position}
selfPositioned={selfPositioned}
>
{estimatePoints?.map((estimate) => (
<CustomSelect.Option key={estimate.id} value={estimate.key} className="capitalize">
<>{estimate.value}</>
</CustomSelect.Option>
))}
</CustomSelect>
);
};

View File

@ -1,4 +1,5 @@
export * from "./assignee";
export * from "./due-date";
export * from "./estimate";
export * from "./priority";
export * from "./state";

View File

@ -1,13 +1,11 @@
import React, { createContext, ReactElement } from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR, { KeyedMutator } from "swr";
// services
import userService from "services/user.service";
// constants
import { CURRENT_USER } from "constants/fetch-keys";
// types
import type { IUser } from "types";
@ -22,18 +20,11 @@ interface IUserContextProps {
export const UserContext = createContext<IUserContextProps>({} as IUserContextProps);
export const UserProvider = ({ children }: { children: ReactElement }) => {
const router = useRouter();
// API to fetch user information
const { data, error, mutate } = useSWR<IUser>(CURRENT_USER, () => userService.currentUser(), {
shouldRetryOnError: false,
});
if (error) {
router.push("/signin");
return null;
}
return (
<UserContext.Provider
value={{

View File

@ -0,0 +1,49 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import estimatesService from "services/estimates.service";
// hooks
import useProjectDetails from "hooks/use-project-details";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
const useEstimateOption = (estimateKey?: number) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails();
const { data: estimatePoints, error: estimatePointsError } = useSWR(
workspaceSlug && projectId && projectDetails && projectDetails?.estimate
? ESTIMATE_POINTS_LIST(projectDetails.estimate as string)
: null,
workspaceSlug && projectId && projectDetails && projectDetails.estimate
? () =>
estimatesService.getEstimatesPointsList(
workspaceSlug as string,
projectId as string,
projectDetails.estimate
)
: null
);
const estimateValue: any =
(estimateKey && estimatePoints?.find((e) => e.key === estimateKey)?.value) ?? "None";
useEffect(() => {
if (estimatePointsError?.status === 404) router.push("/404");
else if (estimatePointsError) router.push("/error");
}, [estimatePointsError, router]);
return {
isEstimateActive: projectDetails?.estimate ? true : false,
estimatePoints,
estimateValue,
};
};
export default useEstimateOption;

View File

@ -15,6 +15,7 @@ const initialValues: Properties = {
priority: false,
state: true,
sub_issue_count: false,
estimate: false,
};
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
@ -90,6 +91,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
priority: properties.priority,
state: properties.state,
sub_issue_count: properties.sub_issue_count,
estimate: properties.estimate,
};
return [newProperties, updateIssueProperties] as const;

View File

@ -24,6 +24,7 @@ const initialValues: Properties = {
priority: false,
state: true,
sub_issue_count: false,
estimate: false,
};
// TODO: Refactor this logic

View File

@ -0,0 +1,42 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
const useProjectDetails = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const {
data: projectDetails,
error: projectDetailsError,
mutate: mutateProjectDetails,
} = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
useEffect(() => {
if (projectDetailsError?.status === 404) {
router.push("/404");
} else if (projectDetailsError) {
router.push("/error");
}
}, [projectDetailsError, router]);
return {
projectDetails,
projectDetailsError,
mutateProjectDetails,
};
};
export default useProjectDetails;

View File

@ -1,40 +1,11 @@
import { useContext, useEffect } from "react";
import { useRouter } from "next/router";
import { useContext } from "react";
// context
import { UserContext } from "contexts/user.context";
interface useUserOptions {
redirectTo?: string;
}
const useUser = (options: useUserOptions = {}) => {
// props
const { redirectTo = null } = options;
const useUser = () => {
// context
const contextData = useContext(UserContext);
// router
const router = useRouter();
/**
* Checks for redirect url and user details from the API.
* if the user is not authenticated, user will be redirected
* to the provided redirectTo route.
*/
useEffect(() => {
if (!contextData?.user || !redirectTo) return;
if (!contextData?.user) {
if (redirectTo) {
router?.pathname !== redirectTo && router.push(redirectTo);
}
router?.pathname !== "/signin" && router.push("/signin");
}
if (contextData?.user) {
if (redirectTo) {
router?.pathname !== redirectTo && router.push(redirectTo);
}
}
}, [contextData?.user, redirectTo, router]);
return { ...contextData };
};

View File

@ -5,13 +5,12 @@ import { useRouter } from "next/router";
// contexts
import { useProjectMyMembership, ProjectMemberProvider } from "contexts/project-member.context";
// hooks
import useUser from "hooks/use-user";
// layouts
import Container from "layouts/container";
import AppHeader from "layouts/app-layout/app-header";
import AppSidebar from "layouts/app-layout/app-sidebar";
import SettingsNavbar from "layouts/settings-navbar";
import { WorkspaceAuthorizationLayout } from "./workspace-authorization-wrapper";
// components
import { NotAuthorizedView, JoinProject } from "components/auth-screens";
import { CommandPalette } from "components/command-palette";
@ -59,83 +58,87 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const user = useUser();
const { loading, error, memberRole: memberType } = useProjectMyMembership();
const settingsLayout = router.pathname.includes("/settings");
return (
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{loading ? (
<div className="container h-screen flex justify-center items-center p-4 text-2xl font-semibold">
<p>Loading...</p>
</div>
) : error?.status === 401 || error?.status === 403 ? (
<JoinProject />
) : error?.status === 404 ? (
<div className="container h-screen grid place-items-center">
<div className="text-center space-y-4">
<p className="text-2xl font-semibold">No such project exist. Create one?</p>
<PrimaryButton
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
Create project
</PrimaryButton>
<WorkspaceAuthorizationLayout>
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{loading ? (
<div className="container h-screen flex justify-center items-center p-4 text-2xl font-semibold">
<p>Loading...</p>
</div>
</div>
) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
actionButton={
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
<a>
<PrimaryButton className="flex items-center gap-1">
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to issues
</PrimaryButton>
</a>
</Link>
}
type="project"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
left={left}
right={right}
setToggleSidebar={setToggleSidebar}
/>
)}
<div
className={`flex w-full flex-grow flex-col ${
noPadding ? "" : settingsLayout ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
}`}
>
{settingsLayout && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-gray-600">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
) : error?.status === 401 || error?.status === 403 ? (
<JoinProject />
) : error?.status === 404 ? (
<div className="container h-screen grid place-items-center">
<div className="text-center space-y-4">
<p className="text-2xl font-semibold">No such project exist. Create one?</p>
<PrimaryButton
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
Create project
</PrimaryButton>
</div>
</div>
) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
actionButton={
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
<a>
<PrimaryButton className="flex items-center gap-1">
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to issues
</PrimaryButton>
</a>
</Link>
}
type="project"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
left={left}
right={right}
setToggleSidebar={setToggleSidebar}
/>
)}
{children}
</div>
</main>
)}
</div>
</Container>
<div
className={`flex w-full flex-grow flex-col ${
noPadding ? "" : settingsLayout ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-primary"
: bg === "secondary"
? "bg-secondary"
: "bg-primary"
}`}
>
{settingsLayout && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-gray-600">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
)}
{children}
</div>
</main>
)}
</div>
</Container>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -0,0 +1,28 @@
import useSWR from "swr";
import { CURRENT_USER } from "constants/fetch-keys";
import userService from "services/user.service";
import { useRouter } from "next/router";
type Props = {
children: React.ReactNode;
};
export const UserAuthorizationLayout: React.FC<Props> = ({ children }) => {
const router = useRouter();
const { data: currentUser, error } = useSWR(CURRENT_USER, () => userService.currentUser());
if (!currentUser && !error) {
return <div className="grid place-items-center h-screen">Loading...</div>;
}
if (error?.status === 401) {
const redirectTo = router.asPath;
router.push(`/signin?next=${redirectTo}`);
return null;
}
return <>{children}</>;
};

View File

@ -7,8 +7,6 @@ import useSWR from "swr";
// services
import workspaceServices from "services/workspace.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import Container from "layouts/container";
import AppSidebar from "layouts/app-layout/app-sidebar";
@ -22,6 +20,7 @@ import { PrimaryButton } from "components/ui";
import { LayerDiagonalIcon } from "components/icons";
// fetch-keys
import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
import { UserAuthorizationLayout } from "./user-authorization-wrapper";
type Meta = {
title?: string | null;
@ -58,8 +57,6 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug } = router.query;
const user = useUser();
const { data: workspaceMemberMe, error } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceServices.workspaceMemberMe(workspaceSlug.toString()) : null,
@ -99,60 +96,66 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
};
return (
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
actionButton={
<Link href={`/${workspaceSlug}`}>
<a>
<PrimaryButton className="flex items-center gap-1">
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace
</PrimaryButton>
</a>
</Link>
}
type="workspace"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
left={left}
right={right}
setToggleSidebar={setToggleSidebar}
/>
)}
<div
className={`flex w-full flex-grow flex-col ${
noPadding ? "" : settingsLayout || profilePage ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
}`}
>
{(settingsLayout || profilePage) && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">
{profilePage ? "Profile" : "Workspace"} Settings
</h3>
<p className="mt-1 text-gray-600">
{profilePage
? "This information will be visible to only you."
: "This information will be displayed to every member of the workspace."}
</p>
</div>
<SettingsNavbar profilePage={profilePage} />
</div>
<UserAuthorizationLayout>
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
actionButton={
<Link href={`/${workspaceSlug}`}>
<a>
<PrimaryButton className="flex items-center gap-1">
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace
</PrimaryButton>
</a>
</Link>
}
type="workspace"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
left={left}
right={right}
setToggleSidebar={setToggleSidebar}
/>
)}
{children}
</div>
</main>
)}
</div>
</Container>
<div
className={`flex w-full flex-grow flex-col ${
noPadding ? "" : settingsLayout || profilePage ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-primary"
: bg === "secondary"
? "bg-secondary"
: "bg-primary"
}`}
>
{(settingsLayout || profilePage) && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">
{profilePage ? "Profile" : "Workspace"} Settings
</h3>
<p className="mt-1 text-gray-600">
{profilePage
? "This information will be visible to only you."
: "This information will be displayed to every member of the workspace."}
</p>
</div>
<SettingsNavbar profilePage={profilePage} />
</div>
)}
{children}
</div>
</main>
)}
</div>
</Container>
</UserAuthorizationLayout>
);
};

View File

@ -6,13 +6,12 @@ import useSWR, { mutate } from "swr";
// services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
// hooks
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates";
//hooks
import useToast from "hooks/use-toast";
// ui
@ -21,10 +20,10 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IEstimate, IProject } from "types";
import { IEstimate } from "types";
import type { NextPage } from "next";
// fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { ESTIMATES_LIST } from "constants/fetch-keys";
const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -32,14 +31,14 @@ const EstimatesSettings: NextPage = () => {
const [isUpdating, setIsUpdating] = useState(false);
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
const [activeEstimate, setActiveEstimate] = useState<IEstimate | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const scollToRef = useRef<HTMLDivElement>(null);
const { projectDetails } = useProjectDetails();
const scrollToRef = useRef<HTMLDivElement>(null);
const { data: estimatesList } = useSWR<IEstimate[]>(
workspaceSlug && projectId ? ESTIMATES_LIST(projectId as string) : null,
@ -74,13 +73,6 @@ const EstimatesSettings: NextPage = () => {
});
};
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
return (
<>
<ProjectAuthorizationWrapper
@ -122,7 +114,6 @@ const EstimatesSettings: NextPage = () => {
</div>
</div>
</section>
<hr className="h-[1px] w-full mt-4" />
{estimatesList && estimatesList.length > 0 && (
<section className="mt-4 divide-y px-6 mb-8 rounded-xl border bg-white">
<>
@ -131,8 +122,6 @@ const EstimatesSettings: NextPage = () => {
<SingleEstimate
key={estimate.id}
estimate={estimate}
activeEstimate={activeEstimate}
setActiveEstimate={setActiveEstimate}
editEstimate={(estimate) => editEstimate(estimate)}
handleEstimateDelete={(estimateId) => removeEstimate(estimateId)}
/>

View File

@ -3,14 +3,13 @@ import React from "react";
import { useRouter } from "next/router";
import Image from "next/image";
// constants
import { requiredAuth } from "lib/auth";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// images
import Logo from "public/onboarding/logo.svg";
// types
import type { NextPage, NextPageContext } from "next";
import type { NextPage } from "next";
// constants
import { CreateWorkspaceForm } from "components/workspace";
@ -23,42 +22,23 @@ const CreateWorkspace: NextPage = () => {
};
return (
<DefaultLayout>
<div className="grid h-full place-items-center p-5">
<div className="w-full space-y-4">
<div className="mb-8 text-center">
<Image src={Logo} height="50" alt="Plane Logo" />
<UserAuthorizationLayout>
<DefaultLayout>
<div className="grid h-full place-items-center p-5">
<div className="w-full space-y-4">
<div className="mb-8 text-center">
<Image src={Logo} height="50" alt="Plane Logo" />
</div>
<CreateWorkspaceForm
defaultValues={defaultValues}
setDefaultValues={() => {}}
onSubmit={(res) => router.push(`/${res.slug}`)}
/>
</div>
<CreateWorkspaceForm
defaultValues={defaultValues}
setDefaultValues={() => {}}
onSubmit={(res) => router.push(`/${res.slug}`)}
/>
</div>
</div>
</DefaultLayout>
</DefaultLayout>
</UserAuthorizationLayout>
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
const user = await requiredAuth(ctx.req?.headers.cookie);
const redirectAfterSignIn = ctx.req?.url;
if (!user) {
return {
redirect: {
destination: `/signin?next=${redirectAfterSignIn}`,
permanent: false,
},
};
}
return {
props: {
user,
},
};
};
export default CreateWorkspace;

View File

@ -1,10 +1,10 @@
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// lib
import { requiredAuth } from "lib/auth";
// services
import workspaceService from "services/workspace.service";
// hooks
@ -12,6 +12,7 @@ import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// components
import SingleInvitation from "components/workspace/single-invitation";
// ui
@ -19,7 +20,7 @@ import { Spinner, EmptySpace, EmptySpaceItem, SecondaryButton, PrimaryButton } f
// icons
import { CubeIcon, PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage, NextPageContext } from "next";
import type { NextPage } from "next";
import type { IWorkspaceMemberInvitation } from "types";
const OnBoard: NextPage = () => {
@ -77,115 +78,96 @@ const OnBoard: NextPage = () => {
};
return (
<DefaultLayout
meta={{
title: "Plane - Welcome to Plane",
description:
"Please fasten your seatbelts because we are about to take your productivity to the next level.",
}}
>
<div className="flex min-h-full flex-col items-center justify-center p-4 sm:p-0">
{user && (
<div className="mb-10 w-96 rounded-lg bg-indigo-100 p-2 text-theme">
<p className="text-center text-sm">logged in as {user.email}</p>
</div>
)}
<div className="w-full rounded-lg p-8 md:w-2/3 lg:w-1/3">
{invitations && workspaces ? (
invitations.length > 0 ? (
<div>
<h2 className="text-lg font-medium text-gray-900">Workspace Invitations</h2>
<p className="mt-1 text-sm text-gray-500">
Select invites that you want to accept.
</p>
<ul role="list" className="mt-6 divide-y divide-gray-200 border-t border-b">
{invitations.map((invitation) => (
<SingleInvitation
key={invitation.id}
invitation={invitation}
invitationsRespond={invitationsRespond}
handleInvitation={handleInvitation}
/>
))}
</ul>
<div className="mt-6 flex items-center gap-2">
<Link href="/">
<a className="w-full">
<SecondaryButton className="w-full">Go to Home</SecondaryButton>
</a>
</Link>
<PrimaryButton className="w-full" onClick={submitInvitations}>
Accept and Continue
</PrimaryButton>
</div>
</div>
) : workspaces && workspaces.length > 0 ? (
<div className="mt-3 flex flex-col gap-y-3">
<h2 className="mb-4 text-2xl font-medium">Your workspaces</h2>
{workspaces.map((workspace) => (
<Link key={workspace.id} href={workspace.slug}>
<a>
<div className="mb-2 flex items-center justify-between rounded border px-4 py-2">
<div className="flex items-center gap-x-2">
<CubeIcon className="h-5 w-5 text-gray-400" />
{workspace.name}
</div>
<div className="flex items-center gap-x-2">
<p className="text-sm">{workspace.owner.first_name}</p>
</div>
</div>
</a>
</Link>
))}
</div>
) : (
invitations.length === 0 &&
workspaces.length === 0 && (
<EmptySpace
title="You don't have any workspaces yet"
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem
Icon={PlusIcon}
title={"Create your Workspace"}
action={() => {
router.push("/create-workspace");
}}
/>
</EmptySpace>
)
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
<UserAuthorizationLayout>
<DefaultLayout
meta={{
title: "Plane - Welcome to Plane",
description:
"Please fasten your seatbelts because we are about to take your productivity to the next level.",
}}
>
<div className="flex min-h-full flex-col items-center justify-center p-4 sm:p-0">
{user && (
<div className="mb-10 w-96 rounded-lg bg-indigo-100 p-2 text-theme">
<p className="text-center text-sm">logged in as {user.email}</p>
</div>
)}
<div className="w-full rounded-lg p-8 md:w-2/3 lg:w-1/3">
{invitations && workspaces ? (
invitations.length > 0 ? (
<div>
<h2 className="text-lg font-medium text-gray-900">Workspace Invitations</h2>
<p className="mt-1 text-sm text-gray-500">
Select invites that you want to accept.
</p>
<ul role="list" className="mt-6 divide-y divide-gray-200 border-t border-b">
{invitations.map((invitation) => (
<SingleInvitation
key={invitation.id}
invitation={invitation}
invitationsRespond={invitationsRespond}
handleInvitation={handleInvitation}
/>
))}
</ul>
<div className="mt-6 flex items-center gap-2">
<Link href="/">
<a className="w-full">
<SecondaryButton className="w-full">Go to Home</SecondaryButton>
</a>
</Link>
<PrimaryButton className="w-full" onClick={submitInvitations}>
Accept and Continue
</PrimaryButton>
</div>
</div>
) : workspaces && workspaces.length > 0 ? (
<div className="mt-3 flex flex-col gap-y-3">
<h2 className="mb-4 text-2xl font-medium">Your workspaces</h2>
{workspaces.map((workspace) => (
<Link key={workspace.id} href={workspace.slug}>
<a>
<div className="mb-2 flex items-center justify-between rounded border px-4 py-2">
<div className="flex items-center gap-x-2">
<CubeIcon className="h-5 w-5 text-gray-400" />
{workspace.name}
</div>
<div className="flex items-center gap-x-2">
<p className="text-sm">{workspace.owner.first_name}</p>
</div>
</div>
</a>
</Link>
))}
</div>
) : (
invitations.length === 0 &&
workspaces.length === 0 && (
<EmptySpace
title="You don't have any workspaces yet"
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem
Icon={PlusIcon}
title={"Create your Workspace"}
action={() => {
router.push("/create-workspace");
}}
/>
</EmptySpace>
)
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</div>
</div>
</div>
</DefaultLayout>
</DefaultLayout>
</UserAuthorizationLayout>
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
const user = await requiredAuth(ctx.req?.headers.cookie);
const redirectAfterSignIn = ctx.req?.url;
if (!user) {
return {
redirect: {
destination: `/signin?next=${redirectAfterSignIn}`,
permanent: false,
},
};
}
return {
props: {
user,
},
};
};
export default OnBoard;

View File

@ -3,14 +3,13 @@ import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// lib
import { requiredAuth } from "lib/auth";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// components
import { InviteMembers, OnboardingCard, UserDetails, Workspace } from "components/onboarding";
// ui
@ -20,7 +19,7 @@ import { ONBOARDING_CARDS } from "constants/workspace";
// images
import Logo from "public/onboarding/logo.svg";
// types
import type { NextPage, GetServerSidePropsContext } from "next";
import type { NextPage } from "next";
const Onboarding: NextPage = () => {
const [step, setStep] = useState(1);
@ -33,83 +32,64 @@ const Onboarding: NextPage = () => {
const { user } = useUser();
return (
<DefaultLayout>
<div className="grid h-full place-items-center p-5">
{step <= 3 ? (
<div className="w-full">
<div className="text-center mb-8">
<Image src={Logo} height="50" alt="Plane Logo" />
</div>
{step === 1 ? (
<UserDetails user={user} setStep={setStep} setUserRole={setUserRole} />
) : step === 2 ? (
<Workspace setStep={setStep} setWorkspace={setWorkspace} />
) : (
<InviteMembers setStep={setStep} workspace={workspace} />
)}
</div>
) : (
<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-white px-14 py-10 text-center shadow-md">
{step === 4 ? (
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
) : step === 5 ? (
<OnboardingCard data={ONBOARDING_CARDS.issue} />
) : step === 6 ? (
<OnboardingCard data={ONBOARDING_CARDS.cycle} />
) : step === 7 ? (
<OnboardingCard data={ONBOARDING_CARDS.module} />
<UserAuthorizationLayout>
<DefaultLayout>
<div className="grid h-full place-items-center p-5">
{step <= 3 ? (
<div className="w-full">
<div className="text-center mb-8">
<Image src={Logo} height="50" alt="Plane Logo" />
</div>
{step === 1 ? (
<UserDetails user={user} setStep={setStep} setUserRole={setUserRole} />
) : step === 2 ? (
<Workspace setStep={setStep} setWorkspace={setWorkspace} />
) : (
<OnboardingCard data={ONBOARDING_CARDS.commandMenu} />
<InviteMembers setStep={setStep} workspace={workspace} />
)}
<div className="mx-auto flex h-1/4 items-end lg:w-1/2">
<PrimaryButton
type="button"
className="flex w-full items-center justify-center text-center "
size="md"
onClick={() => {
if (step === 8) {
userService
.updateUserOnBoard({ userRole })
.then(() => {
router.push("/");
})
.catch((err) => {
console.log(err);
});
} else setStep((prevData) => prevData + 1);
}}
>
{step === 4 || step === 8 ? "Get Started" : "Next"}
</PrimaryButton>
</div>
) : (
<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-white px-14 py-10 text-center shadow-md">
{step === 4 ? (
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
) : step === 5 ? (
<OnboardingCard data={ONBOARDING_CARDS.issue} />
) : step === 6 ? (
<OnboardingCard data={ONBOARDING_CARDS.cycle} />
) : step === 7 ? (
<OnboardingCard data={ONBOARDING_CARDS.module} />
) : (
<OnboardingCard data={ONBOARDING_CARDS.commandMenu} />
)}
<div className="mx-auto flex h-1/4 items-end lg:w-1/2">
<PrimaryButton
type="button"
className="flex w-full items-center justify-center text-center "
size="md"
onClick={() => {
if (step === 8) {
userService
.updateUserOnBoard({ userRole })
.then(() => {
router.push("/");
})
.catch((err) => {
console.log(err);
});
} else setStep((prevData) => prevData + 1);
}}
>
{step === 4 || step === 8 ? "Get Started" : "Next"}
</PrimaryButton>
</div>
</div>
</div>
</div>
)}
</div>
</DefaultLayout>
)}
</div>
</DefaultLayout>
</UserAuthorizationLayout>
);
};
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const user = await requiredAuth(ctx.req?.headers.cookie);
const redirectAfterSignIn = ctx.resolvedUrl;
if (!user) {
return {
redirect: {
destination: `/signin?next=${redirectAfterSignIn}`,
permanent: false,
},
};
}
return {
props: {
user,
},
};
};
export default Onboarding;

View File

@ -30,12 +30,10 @@ class UserService extends APIService {
}
async currentUser(): Promise<any> {
if (!this.getAccessToken()) return null;
return this.get("/api/users/me/")
.then((response) => response?.data)
.catch((error) => {
this.purgeAccessToken();
throw error?.response?.data;
throw error?.response;
});
}

View File

@ -186,6 +186,7 @@ export type Properties = {
priority: boolean;
state: boolean;
sub_issue_count: boolean;
estimate: boolean;
};
export interface IIssueLabels {
@ -272,4 +273,4 @@ export interface IIssueAttachment {
updated_at: string;
updated_by: string;
workspace: string;
}
}