forked from github/plane
chore: user auth layer (#749)
* chore: use estimate points hook created * chore: user auth layer * fix: build error
This commit is contained in:
parent
3fe32606a9
commit
1026ae3eb1
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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";
|
||||
|
69
apps/app/components/issues/view-select/estimate.tsx
Normal file
69
apps/app/components/issues/view-select/estimate.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
export * from "./assignee";
|
||||
export * from "./due-date";
|
||||
export * from "./estimate";
|
||||
export * from "./priority";
|
||||
export * from "./state";
|
||||
|
@ -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={{
|
||||
|
49
apps/app/hooks/use-estimate-option.tsx
Normal file
49
apps/app/hooks/use-estimate-option.tsx
Normal 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;
|
@ -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;
|
||||
|
@ -24,6 +24,7 @@ const initialValues: Properties = {
|
||||
priority: false,
|
||||
state: true,
|
||||
sub_issue_count: false,
|
||||
estimate: false,
|
||||
};
|
||||
|
||||
// TODO: Refactor this logic
|
||||
|
42
apps/app/hooks/use-project-details.tsx
Normal file
42
apps/app/hooks/use-project-details.tsx
Normal 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;
|
@ -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 };
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
28
apps/app/layouts/auth-layout/user-authorization-wrapper.tsx
Normal file
28
apps/app/layouts/auth-layout/user-authorization-wrapper.tsx
Normal 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}</>;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
3
apps/app/types/issues.d.ts
vendored
3
apps/app/types/issues.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user