fix: implementing layouts using _app.tsx get layout method. (#2620)

* fix: implementing layouts in all pages

* fix: layout fixes, implemting using standard nextjs parctice
This commit is contained in:
sriram veeraghanta 2023-11-02 23:57:44 +05:30 committed by GitHub
parent a582021f2c
commit 3c884fd46e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1653 additions and 1423 deletions

View File

@ -1,5 +1,4 @@
import * as React from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
@ -7,16 +6,18 @@ import { Plus } from "lucide-react";
import { Breadcrumbs, BreadcrumbItem, Button } from "@plane/ui"; import { Breadcrumbs, BreadcrumbItem, Button } from "@plane/ui";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
export interface ICyclesHeader {}
export interface ICyclesHeader { export const CyclesHeader: FC<ICyclesHeader> = (props) => {
name: string | undefined; const {} = props;
}
export const CyclesHeader: React.FC<ICyclesHeader> = (props) => {
const { name } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store
const { project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
return ( return (
<div <div
@ -34,7 +35,7 @@ export const CyclesHeader: React.FC<ICyclesHeader> = (props) => {
</Link> </Link>
} }
/> />
<BreadcrumbItem title={`${truncateText(name ?? "Project", 32)} Cycles`} /> <BreadcrumbItem title={`${truncateText(currentProjectDetails?.name ?? "Project Title", 32)} Cycles`} />
</Breadcrumbs> </Breadcrumbs>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import React, { Fragment, useEffect } from "react"; import React, { Fragment, useEffect, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
@ -18,10 +18,12 @@ import { Plus } from "lucide-react";
import emptyAnalytics from "public/empty-state/analytics.svg"; import emptyAnalytics from "public/empty-state/analytics.svg";
// constants // constants
import { ANALYTICS_TABS } from "constants/analytics"; import { ANALYTICS_TABS } from "constants/analytics";
// type
import { NextPageWithLayout } from "types/app";
const trackEventService = new TrackEventService(); const trackEventService = new TrackEventService();
const AnalyticsPage = observer(() => { const AnalyticsPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -110,4 +112,8 @@ const AnalyticsPage = observer(() => {
); );
}); });
AnalyticsPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<WorkspaceAnalyticsHeader />}>{page}</AppLayout>;
};
export default AnalyticsPage; export default AnalyticsPage;

View File

@ -1,14 +1,16 @@
import type { NextPage } from "next"; import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { WorkspaceDashboardView } from "components/views"; import { WorkspaceDashboardView } from "components/views";
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard"; import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
// types
import { NextPageWithLayout } from "types/app";
const WorkspacePage: NextPage = () => ( const WorkspacePage: NextPageWithLayout = () => <WorkspaceDashboardView />;
<AppLayout header={<WorkspaceDashboardHeader />}>
<WorkspaceDashboardView /> WorkspacePage.getLayout = function getLayout(page: ReactElement) {
</AppLayout> return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>;
); };
export default WorkspacePage; export default WorkspacePage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
@ -18,10 +19,12 @@ import { ExternalLinkIcon, Loader } from "@plane/ui";
import { USER_ACTIVITY } from "constants/fetch-keys"; import { USER_ACTIVITY } from "constants/fetch-keys";
// helper // helper
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// type
import { NextPageWithLayout } from "types/app";
const userService = new UserService(); const userService = new UserService();
const ProfileActivity = () => { const ProfileActivityPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -31,165 +34,169 @@ const ProfileActivity = () => {
); );
return ( return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile Activity" />}> <>
<WorkspaceSettingLayout> {userActivity ? (
{userActivity ? ( <section className="pr-9 py-8 w-full overflow-y-auto">
<section className="pr-9 py-8 w-full overflow-y-auto"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <h3 className="text-xl font-medium">Activity</h3>
<h3 className="text-xl font-medium">Activity</h3> </div>
</div> <div className={`flex flex-col gap-2 py-4 w-full`}>
<div className={`flex flex-col gap-2 py-4 w-full`}> <ul role="list" className="-mb-4">
<ul role="list" className="-mb-4"> {userActivity.results.map((activityItem: any) => {
{userActivity.results.map((activityItem: any) => { if (activityItem.field === "comment") {
if (activityItem.field === "comment") { return (
return ( <div key={activityItem.id} className="mt-2">
<div key={activityItem.id} className="mt-2"> <div className="relative flex items-start space-x-3">
<div className="relative flex items-start space-x-3"> <div className="relative px-1">
<div className="relative px-1"> {activityItem.field ? (
{activityItem.field ? ( activityItem.new_value === "restore" && (
activityItem.new_value === "restore" && ( <History className="h-3.5 w-3.5 text-custom-text-200" />
<History className="h-3.5 w-3.5 text-custom-text-200" /> )
) ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( <img
<img src={activityItem.actor_detail.avatar}
src={activityItem.actor_detail.avatar} alt={activityItem.actor_detail.display_name}
alt={activityItem.actor_detail.display_name} height={30}
height={30} width={30}
width={30} className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white" />
/> ) : (
) : ( <div
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`} >
> {activityItem.actor_detail.display_name?.charAt(0)}
{activityItem.actor_detail.display_name?.charAt(0)} </div>
</div> )}
)}
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> <span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
<MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" /> <MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" />
</span> </span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name + " Bot"
: activityItem.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(activityItem.created_at)}
</p>
</div> </div>
<div className="min-w-0 flex-1"> <div className="issue-comments-section p-0">
<div> <RichReadOnlyEditor
<div className="text-xs"> value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
{activityItem.actor_detail.is_bot customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
? activityItem.actor_detail.first_name + " Bot" noBorder
: activityItem.actor_detail.display_name} borderOnFocus={false}
</div> />
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(activityItem.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
<RichReadOnlyEditor
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
noBorder
borderOnFocus={false}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
); </div>
} );
}
const message = const message =
activityItem.verb === "created" && activityItem.verb === "created" &&
activityItem.field !== "cycles" && activityItem.field !== "cycles" &&
activityItem.field !== "modules" && activityItem.field !== "modules" &&
activityItem.field !== "attachment" && activityItem.field !== "attachment" &&
activityItem.field !== "link" && activityItem.field !== "link" &&
activityItem.field !== "estimate" ? ( activityItem.field !== "estimate" ? (
<span className="text-custom-text-200"> <span className="text-custom-text-200">
created{" "} created{" "}
<Link href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}> <Link href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}>
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ExternalLinkIcon className="ml-1 h-3.5 w-3.5" /> this issue. <ExternalLinkIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
</span> </span>
) : activityItem.field ? ( ) : activityItem.field ? (
<ActivityMessage activity={activityItem} showIssue /> <ActivityMessage activity={activityItem} showIssue />
) : ( ) : (
"created the issue." "created the issue."
); );
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="relative pb-1">
<div className="relative flex items-center space-x-2"> <div className="relative flex items-center space-x-2">
<> <>
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="flex h-6 w-6 items-center justify-center"> <div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? ( {activityItem.field ? (
activityItem.new_value === "restore" ? ( activityItem.new_value === "restore" ? (
<History className="h-5 w-5 text-custom-text-200" /> <History className="h-5 w-5 text-custom-text-200" />
) : (
<ActivityIcon activity={activityItem} />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full h-full w-full object-cover"
/>
) : ( ) : (
<div <ActivityIcon activity={activityItem} />
className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`} )
> ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
{activityItem.actor_detail.display_name?.charAt(0)} <img
</div> src={activityItem.actor_detail.avatar}
)} alt={activityItem.actor_detail.display_name}
</div> height={24}
width={24}
className="rounded-full h-full w-full object-cover"
/>
) : (
<div
className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 py-4 border-b border-custom-border-200"> </div>
<div className="text-sm text-custom-text-200 break-words"> <div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( <div className="text-sm text-custom-text-200 break-words">
<span className="text-gray font-medium">Plane</span> {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
) : activityItem.actor_detail.is_bot ? ( <span className="text-gray font-medium">Plane</span>
<span className="text-gray font-medium"> ) : activityItem.actor_detail.is_bot ? (
{activityItem.actor_detail.first_name} Bot <span className="text-gray font-medium">
</span> {activityItem.actor_detail.first_name} Bot
) : ( </span>
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}> ) : (
<a className="text-gray font-medium">{activityItem.actor_detail.display_name}</a> <Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
</Link> <a className="text-gray font-medium">{activityItem.actor_detail.display_name}</a>
)}{" "} </Link>
{message}{" "} )}{" "}
<span className="whitespace-nowrap">{timeAgo(activityItem.created_at)}</span> {message} <span className="whitespace-nowrap">{timeAgo(activityItem.created_at)}</span>
</div>
</div> </div>
</> </div>
</div> </>
</div> </div>
</li> </div>
); </li>
} );
})} }
</ul> })}
</div> </ul>
</section> </div>
) : ( </section>
<Loader className="space-y-5"> ) : (
<Loader.Item height="40px" /> <Loader className="space-y-5">
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> <Loader.Item height="40px" />
)} </Loader>
</WorkspaceSettingLayout> )}
</>
);
};
ProfileActivityPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile Activity" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default ProfileActivity; export default ProfileActivityPage;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -19,8 +19,8 @@ import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane
// icons // icons
import { User2, UserCircle2 } from "lucide-react"; import { User2, UserCircle2 } from "lucide-react";
// types // types
import type { NextPage } from "next";
import type { IUser } from "types"; import type { IUser } from "types";
import type { NextPageWithLayout } from "types/app";
// constants // constants
import { USER_ROLES } from "constants/workspace"; import { USER_ROLES } from "constants/workspace";
import { TIME_ZONES } from "constants/timezones"; import { TIME_ZONES } from "constants/timezones";
@ -38,7 +38,7 @@ const defaultValues: Partial<IUser> = {
const fileService = new FileService(); const fileService = new FileService();
const userService = new UserService(); const userService = new UserService();
const Profile: NextPage = () => { const ProfilePage: NextPageWithLayout = () => {
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// router // router
@ -144,255 +144,261 @@ const Profile: NextPage = () => {
})); }));
return ( return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile" />}> <>
<WorkspaceSettingLayout> <ImageUploadModal
<ImageUploadModal isOpen={isImageUploadModalOpen}
isOpen={isImageUploadModalOpen} onClose={() => setIsImageUploadModalOpen(false)}
onClose={() => setIsImageUploadModalOpen(false)} isRemoving={isRemoving}
isRemoving={isRemoving} handleDelete={() => handleDelete(myProfile?.avatar, true)}
handleDelete={() => handleDelete(myProfile?.avatar, true)} onSuccess={(url) => {
onSuccess={(url) => { setValue("avatar", url);
setValue("avatar", url); handleSubmit(onSubmit)();
handleSubmit(onSubmit)(); setIsImageUploadModalOpen(false);
setIsImageUploadModalOpen(false); }}
}} value={watch("avatar") !== "" ? watch("avatar") : undefined}
value={watch("avatar") !== "" ? watch("avatar") : undefined} userImage
userImage />
/> {myProfile ? (
{myProfile ? ( <form onSubmit={handleSubmit(onSubmit)} className="h-full w-full">
<form onSubmit={handleSubmit(onSubmit)} className="h-full w-full"> <div className={`flex flex-col gap-8 pr-9 py-9 w-full overflow-y-auto`}>
<div className={`flex flex-col gap-8 pr-9 py-9 w-full overflow-y-auto`}> <div className="relative h-44 w-full mt-6">
<div className="relative h-44 w-full mt-6"> <img
<img src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"} className="h-44 w-full rounded-lg object-cover"
className="h-44 w-full rounded-lg object-cover" alt={myProfile?.first_name ?? "Cover image"}
alt={myProfile?.first_name ?? "Cover image"} />
<div className="flex items-end justify-between absolute left-8 -bottom-6">
<div className="flex gap-3">
<div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
<User2 className="h-full w-full text-custom-text-200" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.display_name}
/>
</div>
)}
</button>
</div>
</div>
</div>
<div className="flex absolute right-3 bottom-3">
<Controller
control={control}
name="cover_image"
render={() => (
<ImagePickerPopover
label={"Change cover"}
onChange={(imageUrl) => {
setValue("cover_image", imageUrl);
}}
control={control}
value={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
/>
)}
/> />
<div className="flex items-end justify-between absolute left-8 -bottom-6">
<div className="flex gap-3">
<div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
<User2 className="h-full w-full text-custom-text-200" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.display_name}
/>
</div>
)}
</button>
</div>
</div>
</div>
<div className="flex absolute right-3 bottom-3">
<Controller
control={control}
name="cover_image"
render={() => (
<ImagePickerPopover
label={"Change cover"}
onChange={(imageUrl) => {
setValue("cover_image", imageUrl);
}}
control={control}
value={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
/>
)}
/>
</div>
</div>
<div className="flex item-center justify-between px-8 mt-4">
<div className="flex flex-col">
<div className="flex item-center text-lg font-semibold text-custom-text-100">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div>
<span className="text-sm tracking-tight">{watch("email")}</span>
</div>
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
<span className="h-4 w-4">
<UserCircle2 className="h-4 w-4" />
</span>
View Profile
</a>
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">First Name</h4>
<Controller
control={control}
name="first_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your first name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Controller
control={control}
name="last_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Email</h4>
<Controller
control={control}
name="email"
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
disabled
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Role</h4>
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Controller
control={control}
name="display_name"
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="display_name"
name="display_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.display_name)}
placeholder="Enter your display name"
className="w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Timezone </h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
options={timeZoneOptions}
onChange={onChange}
optionsClassName="w-full"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Profile..." : "Update Profile"}
</Button>
</div>
</div> </div>
</div> </div>
</form>
) : ( <div className="flex item-center justify-between px-8 mt-4">
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="flex flex-col">
<Spinner /> <div className="flex item-center text-lg font-semibold text-custom-text-100">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div>
<span className="text-sm tracking-tight">{watch("email")}</span>
</div>
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
<span className="h-4 w-4">
<UserCircle2 className="h-4 w-4" />
</span>
View Profile
</a>
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">First Name</h4>
<Controller
control={control}
name="first_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your first name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Controller
control={control}
name="last_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Email</h4>
<Controller
control={control}
name="email"
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
disabled
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Role</h4>
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Controller
control={control}
name="display_name"
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="display_name"
name="display_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.display_name)}
placeholder="Enter your display name"
className="w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Timezone </h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
options={timeZoneOptions}
onChange={onChange}
optionsClassName="w-full"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Profile..." : "Update Profile"}
</Button>
</div>
</div>
</div> </div>
)} </form>
</WorkspaceSettingLayout> ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
)}
</>
);
};
ProfilePage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default Profile; export default ProfilePage;

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, ReactElement } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// hooks // hooks
@ -14,8 +14,10 @@ import { WorkspaceSettingHeader } from "components/headers";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// constants // constants
import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes";
// type
import { NextPageWithLayout } from "types/app";
const ProfilePreferencesPage = observer(() => { const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
const { user: userStore } = useMobxStore(); const { user: userStore } = useMobxStore();
// states // states
const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null); const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null);
@ -45,32 +47,38 @@ const ProfilePreferencesPage = observer(() => {
}; };
return ( return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile Preferences" />}> <>
<WorkspaceSettingLayout> {userStore.currentUser ? (
{userStore.currentUser ? ( <div className="pr-9 py-8 w-full overflow-y-auto">
<div className="pr-9 py-8 w-full overflow-y-auto"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <h3 className="text-xl font-medium">Preferences</h3>
<h3 className="text-xl font-medium">Preferences</h3>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200">Select or customize your interface color scheme.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
</div>
</div>
{userTheme?.theme === "custom" && <CustomThemeSelector />}
</div> </div>
) : ( <div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="col-span-12 sm:col-span-6">
<Spinner /> <h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200">Select or customize your interface color scheme.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
</div>
</div> </div>
)} {userTheme?.theme === "custom" && <CustomThemeSelector />}
</WorkspaceSettingLayout> </div>
</AppLayout> ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
)}
</>
); );
}); });
ProfilePreferencesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="My Profile Preferences" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout>
);
};
export default ProfilePreferencesPage; export default ProfilePreferencesPage;

View File

@ -1,5 +1,4 @@
import React from "react"; import React, { ReactElement } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
@ -13,10 +12,10 @@ import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanba
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
// types // types
import { NextPageWithLayout } from "types/app";
const ProfileAssignedIssues: NextPage = observer(() => { const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => {
const { const {
workspace: workspaceStore, workspace: workspaceStore,
project: projectStore, project: projectStore,
@ -45,22 +44,28 @@ const ProfileAssignedIssues: NextPage = observer(() => {
const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout; const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout;
return ( return (
<AppLayout header={<UserProfileHeader />}> <>
<ProfileAuthWrapper showProfileIssuesFilter> {isLoading ? (
{isLoading ? ( <div>Loading...</div>
<div>Loading...</div> ) : (
) : ( <div className="w-full h-full relative overflow-auto -z-1">
<div className="w-full h-full relative overflow-auto -z-1"> {activeLayout === "list" ? (
{activeLayout === "list" ? ( <ProfileIssuesListLayout />
<ProfileIssuesListLayout /> ) : activeLayout === "kanban" ? (
) : activeLayout === "kanban" ? ( <ProfileIssuesKanBanLayout />
<ProfileIssuesKanBanLayout /> ) : null}
) : null} </div>
</div> )}
)} </>
</ProfileAuthWrapper>
</AppLayout>
); );
}); });
export default ProfileAssignedIssues; ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<UserProfileHeader />}>
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
</AppLayout>
);
};
export default ProfileAssignedIssuesPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// store // store
@ -12,9 +12,9 @@ import { UserProfileHeader } from "components/headers";
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ProfileCreatedIssues: NextPage = () => { const ProfileCreatedIssuesPage: NextPageWithLayout = () => {
const { const {
workspace: workspaceStore, workspace: workspaceStore,
project: projectStore, project: projectStore,
@ -39,23 +39,29 @@ const ProfileCreatedIssues: NextPage = () => {
const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout; const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout;
return (
<>
{isLoading ? (
<div>Loading...</div>
) : (
<div className="w-full h-full relative overflow-auto -z-1">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
)}
</>
);
};
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader />}>
<ProfileAuthWrapper showProfileIssuesFilter> <ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
{isLoading ? (
<div>Loading...</div>
) : (
<div className="w-full h-full relative overflow-auto -z-1">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
)}
</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );
}; };
export default observer(ProfileCreatedIssues); export default observer(ProfileCreatedIssuesPage);

View File

@ -1,9 +1,6 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// layouts // layouts
@ -19,8 +16,8 @@ import {
ProfileWorkload, ProfileWorkload,
} from "components/profile"; } from "components/profile";
// types // types
import type { NextPage } from "next";
import { IUserStateDistribution, TStateGroups } from "types"; import { IUserStateDistribution, TStateGroups } from "types";
import { NextPageWithLayout } from "types/app";
// constants // constants
import { USER_PROFILE_DATA } from "constants/fetch-keys"; import { USER_PROFILE_DATA } from "constants/fetch-keys";
import { GROUP_CHOICES } from "constants/project"; import { GROUP_CHOICES } from "constants/project";
@ -28,7 +25,7 @@ import { GROUP_CHOICES } from "constants/project";
// services // services
const userService = new UserService(); const userService = new UserService();
const ProfileOverview: NextPage = () => { const ProfileOverviewPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
@ -44,21 +41,25 @@ const ProfileOverview: NextPage = () => {
else return { state_group: key as TStateGroups, state_count: 0 }; else return { state_group: key as TStateGroups, state_count: 0 };
}); });
return (
<div className="h-full w-full px-5 md:px-9 py-5 space-y-7 overflow-y-auto">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
</div>
<ProfileActivity />
</div>
);
};
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader />}>
<ProfileAuthWrapper> <ProfileAuthWrapper>{page}</ProfileAuthWrapper>
<div className="h-full w-full px-5 md:px-9 py-5 space-y-7 overflow-y-auto">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
</div>
<ProfileActivity />
</div>
</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );
}; };
export default ProfileOverview; export default ProfileOverviewPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// store // store
@ -12,9 +12,9 @@ import { UserProfileHeader } from "components/headers";
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ProfileSubscribedIssues: NextPage = () => { const ProfileSubscribedIssuesPage: NextPageWithLayout = () => {
const { const {
workspace: workspaceStore, workspace: workspaceStore,
project: projectStore, project: projectStore,
@ -58,4 +58,12 @@ const ProfileSubscribedIssues: NextPage = () => {
); );
}; };
export default observer(ProfileSubscribedIssues); ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<UserProfileHeader />}>
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
</AppLayout>
);
};
export default observer(ProfileSubscribedIssuesPage);

View File

@ -1,10 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// services // services
import { IssueService, IssueArchiveService } from "services/issue"; import { IssueService, IssueArchiveService } from "services/issue";
@ -22,7 +18,7 @@ import { ArchiveIcon, Loader } from "@plane/ui";
import { History } from "lucide-react"; import { History } from "lucide-react";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
@ -42,12 +38,13 @@ const defaultValues: Partial<IIssue> = {
const issueService = new IssueService(); const issueService = new IssueService();
const issueArchiveService = new IssueArchiveService(); const issueArchiveService = new IssueArchiveService();
const ArchivedIssueDetailsPage: NextPage = () => { const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
const [isRestoring, setIsRestoring] = useState(false); // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, archivedIssueId } = router.query; const { workspaceSlug, projectId, archivedIssueId } = router.query;
// states
const [isRestoring, setIsRestoring] = useState(false);
// hooks
const { user } = useUserAuth(); const { user } = useUserAuth();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -136,7 +133,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
}; };
return ( return (
<AppLayout header={<ProjectArchivedIssueDetailsHeader />} withProjectWrapper> <>
{issueDetails && projectId ? ( {issueDetails && projectId ? (
<div className="flex h-full overflow-hidden"> <div className="flex h-full overflow-hidden">
<div className="w-2/3 h-full overflow-y-auto space-y-2 divide-y-2 divide-custom-border-300 p-5"> <div className="w-2/3 h-full overflow-y-auto space-y-2 divide-y-2 divide-custom-border-300 p-5">
@ -187,6 +184,14 @@ const ArchivedIssueDetailsPage: NextPage = () => {
</div> </div>
</Loader> </Loader>
)} )}
</>
);
};
ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectArchivedIssueDetailsHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };

View File

@ -1,5 +1,5 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// contexts // contexts
@ -10,33 +10,38 @@ import { ProjectArchivedIssuesHeader } from "components/headers";
// icons // icons
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ProjectArchivedIssues: NextPage = () => { const ProjectArchivedIssuesPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
return (
<div className="h-full w-full flex flex-col">
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<ArchiveIcon className="h-4 w-4" />
<span>Archived Issues</span>
<X className="h-3 w-3" />
</button>
</div>
{/* <IssuesView /> */}
</div>
);
};
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<IssueViewContextProvider> <IssueViewContextProvider>
<AppLayout header={<ProjectArchivedIssuesHeader />} withProjectWrapper> <AppLayout header={<ProjectArchivedIssuesHeader />} withProjectWrapper>
<div className="h-full w-full flex flex-col"> {page}
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<ArchiveIcon className="h-4 w-4" />
<span>Archived Issues</span>
<X className="h-3 w-3" />
</button>
</div>
{/* <IssuesView /> */}
</div>
</AppLayout> </AppLayout>
</IssueViewContextProvider> </IssueViewContextProvider>
); );
}; };
export default ProjectArchivedIssues; export default ProjectArchivedIssuesPage;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
@ -22,10 +22,11 @@ import { EmptyState } from "components/common";
import emptyCycle from "public/empty-state/cycle.svg"; import emptyCycle from "public/empty-state/cycle.svg";
// types // types
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
import { NextPageWithLayout } from "types/app";
const issueService = new IssueService(); const issueService = new IssueService();
const SingleCycle: React.FC = () => { const CycleDetailPage: NextPageWithLayout = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const router = useRouter(); const router = useRouter();
@ -75,7 +76,7 @@ const SingleCycle: React.FC = () => {
}; };
return ( return (
<AppLayout header={<CycleIssuesHeader />} withProjectWrapper> <>
{/* TODO: Update logic to bulk add issues to a cycle */} {/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal <ExistingIssuesListModal
isOpen={cycleIssuesListModal} isOpen={cycleIssuesListModal}
@ -113,8 +114,16 @@ const SingleCycle: React.FC = () => {
</div> </div>
</> </>
)} )}
</>
);
};
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<CycleIssuesHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };
export default SingleCycle; export default CycleDetailPage;

View File

@ -1,4 +1,4 @@
import { Fragment, useCallback, useEffect, useState } from "react"; import { Fragment, useCallback, useEffect, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import useSWR from "swr"; import useSWR from "swr";
@ -13,19 +13,20 @@ import { CyclesHeader } from "components/headers";
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
// ui // ui
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
import { Tooltip } from "@plane/ui";
// images // images
import emptyCycle from "public/empty-state/cycle.svg"; import emptyCycle from "public/empty-state/cycle.svg";
// types // types
import { TCycleView, TCycleLayout } from "types"; import { TCycleView, TCycleLayout } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// constants // constants
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle"; import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
// lib cookie // lib cookie
import { setLocalStorage, getLocalStorage } from "lib/local-storage"; import { setLocalStorage, getLocalStorage } from "lib/local-storage";
import { Tooltip } from "@plane/ui"; // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
const ProjectCyclesPage: NextPage = observer(() => { const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false); const [createModal, setCreateModal] = useState(false);
// store // store
const { project: projectStore, cycle: cycleStore } = useMobxStore(); const { project: projectStore, cycle: cycleStore } = useMobxStore();
@ -85,7 +86,7 @@ const ProjectCyclesPage: NextPage = observer(() => {
const cycleLayout = cycleStore?.cycleLayout; const cycleLayout = cycleStore?.cycleLayout;
return ( return (
<AppLayout header={<CyclesHeader name={currentProjectDetails?.name} />} withProjectWrapper> <>
<CycleCreateUpdateModal <CycleCreateUpdateModal
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -217,8 +218,16 @@ const ProjectCyclesPage: NextPage = observer(() => {
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
)} )}
</AppLayout> </>
); );
}); });
ProjectCyclesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<CyclesHeader />} withProjectWrapper>
{page}
</AppLayout>
);
};
export default ProjectCyclesPage; export default ProjectCyclesPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
@ -8,32 +9,38 @@ import { ProjectDraftIssueHeader } from "components/headers";
// icons // icons
import { X, PenSquare } from "lucide-react"; import { X, PenSquare } from "lucide-react";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ProjectDraftIssues: NextPage = () => { const ProjectDraftIssuesPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
return (
<div className="h-full w-full flex flex-col">
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-3 w-3 text-custom-text-300" />
<span>Draft Issues</span>
<X className="h-3 w-3" />
</button>
</div>
</div>
);
};
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<IssueViewContextProvider> <IssueViewContextProvider>
<AppLayout header={<ProjectDraftIssueHeader />} withProjectWrapper> <AppLayout header={<ProjectDraftIssueHeader />} withProjectWrapper>
<div className="h-full w-full flex flex-col"> {page}
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-3 w-3 text-custom-text-300" />
<span>Draft Issues</span>
<X className="h-3 w-3" />
</button>
</div>
</div>
</AppLayout> </AppLayout>
</IssueViewContextProvider> </IssueViewContextProvider>
); );
}; };
export default ProjectDraftIssues; export default ProjectDraftIssuesPage;

View File

@ -1,6 +1,5 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { NextPage } from "next";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
@ -9,8 +8,10 @@ import { AppLayout } from "layouts/app-layout";
// components // components
import { InboxActionsHeader, InboxMainContent, InboxIssuesListSidebar } from "components/inbox"; import { InboxActionsHeader, InboxMainContent, InboxIssuesListSidebar } from "components/inbox";
import { ProjectInboxHeader } from "components/headers"; import { ProjectInboxHeader } from "components/headers";
// types
import { NextPageWithLayout } from "types/app";
const ProjectInbox: NextPage = () => { const ProjectInboxPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query; const { workspaceSlug, projectId, inboxId } = router.query;
@ -24,18 +25,24 @@ const ProjectInbox: NextPage = () => {
); );
return ( return (
<AppLayout header={<ProjectInboxHeader />} withProjectWrapper> <div className="flex flex-col h-full">
<div className="flex flex-col h-full"> <InboxActionsHeader />
<InboxActionsHeader /> <div className="grid grid-cols-4 flex-1 divide-x divide-custom-border-200 overflow-hidden">
<div className="grid grid-cols-4 flex-1 divide-x divide-custom-border-200 overflow-hidden"> <InboxIssuesListSidebar />
<InboxIssuesListSidebar /> <div className="col-span-3 h-full overflow-auto">
<div className="col-span-3 h-full overflow-auto"> <InboxMainContent />
<InboxMainContent />
</div>
</div> </div>
</div> </div>
</div>
);
};
ProjectInboxPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectInboxHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };
export default ProjectInbox; export default ProjectInboxPage;

View File

@ -1,9 +1,6 @@
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// services // services
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
@ -21,10 +18,9 @@ import { Loader } from "@plane/ui";
import emptyIssue from "public/empty-state/issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
// helper
const defaultValues: Partial<IIssue> = { const defaultValues: Partial<IIssue> = {
description: "", description: "",
@ -42,10 +38,10 @@ const defaultValues: Partial<IIssue> = {
// services // services
const issueService = new IssueService(); const issueService = new IssueService();
const IssueDetailsPage: NextPage = () => { const IssueDetailsPage: NextPageWithLayout = () => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
// console.log(workspaceSlug, "workspaceSlug")
const { user } = useUserAuth(); const { user } = useUserAuth();
@ -111,7 +107,8 @@ const IssueDetailsPage: NextPage = () => {
}, [issueDetails, reset, issueId]); }, [issueDetails, reset, issueId]);
return ( return (
<AppLayout header={<ProjectIssueDetailsHeader />} withProjectWrapper> <>
{" "}
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyIssue} image={emptyIssue}
@ -152,6 +149,14 @@ const IssueDetailsPage: NextPage = () => {
</div> </div>
</Loader> </Loader>
)} )}
</>
);
};
IssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectIssueDetailsHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };

View File

@ -1,17 +1,24 @@
import { ReactElement } from "react";
// components // components
import { ProjectLayoutRoot } from "components/issues"; import { ProjectLayoutRoot } from "components/issues";
import { ProjectIssuesHeader } from "components/headers"; import { ProjectIssuesHeader } from "components/headers";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
const ProjectIssues: NextPage = () => ( const ProjectIssuesPage: NextPageWithLayout = () => (
<AppLayout header={<ProjectIssuesHeader />} withProjectWrapper> <div className="h-full w-full">
<div className="h-full w-full"> <ProjectLayoutRoot />
<ProjectLayoutRoot /> </div>
</div>
</AppLayout>
); );
export default ProjectIssues; ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectIssuesHeader />} withProjectWrapper>
{page}
</AppLayout>
);
};
export default ProjectIssuesPage;

View File

@ -1,9 +1,8 @@
import React, { useState } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { ModuleService } from "services/module.service"; import { ModuleService } from "services/module.service";
// hooks // hooks
@ -22,23 +21,23 @@ import { EmptyState } from "components/common";
// assets // assets
import emptyModule from "public/empty-state/module.svg"; import emptyModule from "public/empty-state/module.svg";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
const moduleService = new ModuleService(); const moduleService = new ModuleService();
const ModuleIssuesPage: NextPage = () => { const ModuleIssuesPage: NextPageWithLayout = () => {
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
// store
const { module: moduleStore } = useMobxStore(); const { module: moduleStore } = useMobxStore();
// hooks
const { user } = useUser(); const { user } = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// local storage
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
@ -78,45 +77,51 @@ const ModuleIssuesPage: NextPage = () => {
return ( return (
<> <>
<AppLayout header={<ModuleIssuesHeader />} withProjectWrapper> {/* TODO: Update logic to bulk add issues to a cycle */}
{/* TODO: Update logic to bulk add issues to a cycle */} <ExistingIssuesListModal
<ExistingIssuesListModal isOpen={moduleIssuesListModal}
isOpen={moduleIssuesListModal} handleClose={() => setModuleIssuesListModal(false)}
handleClose={() => setModuleIssuesListModal(false)} searchParams={{ module: true }}
searchParams={{ module: true }} handleOnSubmit={handleAddIssuesToModule}
handleOnSubmit={handleAddIssuesToModule} />
{error ? (
<EmptyState
image={emptyModule}
title="Module does not exist"
description="The module you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other modules",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/modules`),
}}
/> />
{error ? ( ) : (
<EmptyState <div className="flex h-full w-full">
image={emptyModule} <div className="h-full w-full overflow-hidden">
title="Module does not exist" <ModuleLayoutRoot openIssuesListModal={openIssuesListModal} />
description="The module you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other modules",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/modules`),
}}
/>
) : (
<div className="flex h-full w-full">
<div className="h-full w-full overflow-hidden">
<ModuleLayoutRoot openIssuesListModal={openIssuesListModal} />
</div>
{moduleId && !isSidebarCollapsed && (
<div
className="flex flex-col gap-3.5 h-full w-[24rem] z-10 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 flex-shrink-0"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<ModuleDetailsSidebar moduleId={moduleId.toString()} handleClose={toggleSidebar} />
</div>
)}
</div> </div>
)} {moduleId && !isSidebarCollapsed && (
</AppLayout> <div
className="flex flex-col gap-3.5 h-full w-[24rem] z-10 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 flex-shrink-0"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<ModuleDetailsSidebar moduleId={moduleId.toString()} handleClose={toggleSidebar} />
</div>
)}
</div>
)}
</> </>
); );
}; };
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ModuleIssuesHeader />} withProjectWrapper>
{page}
</AppLayout>
);
};
export default ModuleIssuesPage; export default ModuleIssuesPage;

View File

@ -1,15 +1,20 @@
import React from "react"; import { ReactElement } from "react";
import { NextPage } from "next";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { ModulesListView } from "components/modules"; import { ModulesListView } from "components/modules";
import { ModulesListHeader } from "components/headers"; import { ModulesListHeader } from "components/headers";
// types
import { NextPageWithLayout } from "types/app";
const ProjectModules: NextPage = () => ( const ProjectModulesPage: NextPageWithLayout = () => <ModulesListView />;
<AppLayout header={<ModulesListHeader />} withProjectWrapper>
<ModulesListView />
</AppLayout>
);
export default ProjectModules; ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ModulesListHeader />} withProjectWrapper>
{page}
</AppLayout>
);
};
export default ProjectModulesPage;

View File

@ -1,18 +1,10 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// react-color
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd"; import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { PageService } from "services/page.service"; import { PageService } from "services/page.service";
@ -23,6 +15,7 @@ import useUser from "hooks/use-user";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages"; import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages";
import { CreateLabelModal } from "components/labels"; import { CreateLabelModal } from "components/labels";
import { CreateBlock } from "components/pages/create-block"; import { CreateBlock } from "components/pages/create-block";
@ -39,7 +32,7 @@ import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helpe
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types"; import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types";
// fetch-keys // fetch-keys
import { import {
@ -55,7 +48,7 @@ const projectService = new ProjectService();
const pageService = new PageService(); const pageService = new PageService();
const issueLabelService = new IssueLabelService(); const issueLabelService = new IssueLabelService();
const SinglePage: NextPage = () => { const PageDetailsPage: NextPageWithLayout = () => {
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [showBlock, setShowBlock] = useState(false); const [showBlock, setShowBlock] = useState(false);
@ -302,7 +295,7 @@ const SinglePage: NextPage = () => {
}, [memberDetails]); }, [memberDetails]);
return ( return (
<AppLayout header={<PagesHeader />} withProjectWrapper> <>
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyPage} image={emptyPage}
@ -330,7 +323,7 @@ const SinglePage: NextPage = () => {
<Controller <Controller
name="name" name="name"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={() => (
<TextArea <TextArea
id="name" id="name"
name="name" name="name"
@ -627,8 +620,16 @@ const SinglePage: NextPage = () => {
<Loader.Item height="200px" /> <Loader.Item height="200px" />
</Loader> </Loader>
)} )}
</>
);
};
PageDetailsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<PagesHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };
export default SinglePage; export default PageDetailsPage;

View File

@ -1,9 +1,6 @@
import { useState, Fragment } from "react"; import { useState, Fragment, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
// headless ui
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// hooks // hooks
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
@ -17,8 +14,7 @@ import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "compone
import { PagesHeader } from "components/headers"; import { PagesHeader } from "components/headers";
// types // types
import { TPageViewProps } from "types"; import { TPageViewProps } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys
const AllPagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.AllPagesList), { const AllPagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false, ssr: false,
@ -38,7 +34,7 @@ const OtherPagesList = dynamic<TPagesListProps>(() => import("components/pages")
const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by others"]; const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by others"];
const ProjectPages: NextPage = () => { const ProjectPagesPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// states // states
@ -68,7 +64,7 @@ const ProjectPages: NextPage = () => {
}; };
return ( return (
<AppLayout header={<PagesHeader showButton />} withProjectWrapper> <>
{workspaceSlug && projectId && ( {workspaceSlug && projectId && (
<CreateUpdatePageModal <CreateUpdatePageModal
isOpen={createUpdatePageModal} isOpen={createUpdatePageModal}
@ -160,8 +156,16 @@ const ProjectPages: NextPage = () => {
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</div> </div>
</>
);
};
ProjectPagesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<PagesHeader showButton />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };
export default ProjectPages; export default ProjectPagesPage;

View File

@ -1,9 +1,6 @@
import React from "react"; import React, { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// layouts // layouts
@ -17,7 +14,7 @@ import useToast from "hooks/use-toast";
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
import { IProject } from "types"; import { IProject } from "types";
// constant // constant
import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
@ -25,7 +22,7 @@ import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fet
// services // services
const projectService = new ProjectService(); const projectService = new ProjectService();
const AutomationsSettings: NextPage = () => { const AutomationSettingsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -70,19 +67,23 @@ const AutomationsSettings: NextPage = () => {
const isAdmin = memberDetails?.role === 20; const isAdmin = memberDetails?.role === 20;
return (
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
</section>
);
};
AutomationSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<ProjectSettingHeader title="Automations Settings" />} withProjectWrapper> <AppLayout header={<ProjectSettingHeader title="Automations Settings" />} withProjectWrapper>
<ProjectSettingLayout> <ProjectSettingLayout>{page}</ProjectSettingLayout>
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
</section>
</ProjectSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default AutomationsSettings; export default AutomationSettingsPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -6,16 +6,20 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { EstimatesList } from "components/estimates/estimate-list"; import { EstimatesList } from "components/estimates/estimate-list";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const EstimatesSettings: NextPage = () => ( const EstimatesSettingsPage: NextPageWithLayout = () => (
<AppLayout header={<ProjectSettingHeader title="Estimates Settings" />} withProjectWrapper> <div className="pr-9 py-8 w-full overflow-y-auto">
<ProjectSettingLayout> <EstimatesList />
<div className="pr-9 py-8 w-full overflow-y-auto"> </div>
<EstimatesList />
</div>
</ProjectSettingLayout>
</AppLayout>
); );
export default EstimatesSettings; EstimatesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectSettingHeader title="Estimates Settings" />} withProjectWrapper>
<ProjectSettingLayout>{page}; </ProjectSettingLayout>
</AppLayout>
);
};
export default EstimatesSettingsPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
@ -6,43 +6,43 @@ import { useMobxStore } from "lib/mobx/store-provider";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
// hooks
import useUserAuth from "hooks/use-user-auth";
// components // components
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { ProjectFeaturesList } from "components/project"; import { ProjectFeaturesList } from "components/project";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const FeaturesSettings: NextPage = () => { const FeaturesSettingsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store
const {} = useUserAuth(); const {
user: { fetchUserProjectInfo },
const { user: userStore } = useMobxStore(); } = useMobxStore();
const { data: memberDetails } = useSWR( const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString())
: null
); );
const isAdmin = memberDetails?.role === 20; const isAdmin = memberDetails?.role === 20;
return (
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Features</h3>
</div>
<ProjectFeaturesList />
</section>
);
};
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<ProjectSettingHeader title="Features Settings" />} withProjectWrapper> <AppLayout header={<ProjectSettingHeader title="Features Settings" />} withProjectWrapper>
<ProjectSettingLayout> <ProjectSettingLayout>{page}</ProjectSettingLayout>
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Features</h3>
</div>
<ProjectFeaturesList />
</section>
</ProjectSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default FeaturesSettings; export default FeaturesSettingsPage;

View File

@ -1,6 +1,5 @@
import { useState } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
@ -14,12 +13,12 @@ import {
ProjectDetailsFormLoader, ProjectDetailsFormLoader,
} from "components/project"; } from "components/project";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys // fetch-keys
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
const GeneralSettings: NextPage = observer(() => { const GeneralSettingsPage: NextPageWithLayout = observer(() => {
// store // store
const { project: projectStore } = useMobxStore(); const { project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore; const { currentProjectDetails } = projectStore;
@ -42,37 +41,43 @@ const GeneralSettings: NextPage = observer(() => {
const isAdmin = currentProjectDetails?.member_role === 20; const isAdmin = currentProjectDetails?.member_role === 20;
return ( return (
<AppLayout header={<ProjectSettingHeader title="General Settings" />} withProjectWrapper> <>
<ProjectSettingLayout> {currentProjectDetails && (
{currentProjectDetails && ( <DeleteProjectModal
<DeleteProjectModal project={currentProjectDetails}
isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)}
/>
)}
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
{currentProjectDetails && workspaceSlug ? (
<ProjectDetailsForm
project={currentProjectDetails} project={currentProjectDetails}
isOpen={Boolean(selectProject)} workspaceSlug={workspaceSlug.toString()}
onClose={() => setSelectedProject(null)} isAdmin={isAdmin}
/> />
) : (
<ProjectDetailsFormLoader />
)} )}
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}> {isAdmin && (
{currentProjectDetails && workspaceSlug ? ( <DeleteProjectSection
<ProjectDetailsForm projectDetails={currentProjectDetails}
project={currentProjectDetails} handleDelete={() => setSelectedProject(currentProjectDetails.id ?? null)}
workspaceSlug={workspaceSlug.toString()} />
isAdmin={isAdmin} )}
/> </div>
) : ( </>
<ProjectDetailsFormLoader />
)}
{isAdmin && (
<DeleteProjectSection
projectDetails={currentProjectDetails}
handleDelete={() => setSelectedProject(currentProjectDetails.id ?? null)}
/>
)}
</div>
</ProjectSettingLayout>
</AppLayout>
); );
}); });
export default GeneralSettings; GeneralSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectSettingHeader title="General Settings" />} withProjectWrapper>
<ProjectSettingLayout>{page}</ProjectSettingLayout>
</AppLayout>
);
};
export default GeneralSettingsPage;

View File

@ -1,9 +1,6 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -20,7 +17,7 @@ import { Loader } from "@plane/ui";
import emptyIntegration from "public/empty-state/integration.svg"; import emptyIntegration from "public/empty-state/integration.svg";
// types // types
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
@ -28,7 +25,7 @@ import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
const projectService = new ProjectService(); const projectService = new ProjectService();
const ProjectIntegrations: NextPage = () => { const ProjectIntegrationsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -45,43 +42,47 @@ const ProjectIntegrations: NextPage = () => {
const isAdmin = projectDetails?.member_role === 20; const isAdmin = projectDetails?.member_role === 20;
return ( return (
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="Integrations Settings" />}> <div className={`pr-9 py-8 gap-10 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<ProjectSettingLayout> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<div className={`pr-9 py-8 gap-10 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}> <h3 className="text-xl font-medium">Integrations</h3>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> </div>
<h3 className="text-xl font-medium">Integrations</h3> {workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<div>
{workspaceIntegrations.map((integration) => (
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
))}
</div> </div>
{workspaceIntegrations ? ( ) : (
workspaceIntegrations.length > 0 ? ( <EmptyState
<div> title="You haven't configured integrations"
{workspaceIntegrations.map((integration) => ( description="Configure GitHub and other integrations to sync your project issues."
<IntegrationCard key={integration.integration_detail.id} integration={integration} /> image={emptyIntegration}
))} primaryButton={{
</div> text: "Configure now",
) : ( onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
<EmptyState }}
title="You haven't configured integrations" disabled={!isAdmin}
description="Configure GitHub and other integrations to sync your project issues." />
image={emptyIntegration} )
primaryButton={{ ) : (
text: "Configure now", <Loader className="space-y-5">
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`), <Loader.Item height="40px" />
}} <Loader.Item height="40px" />
disabled={!isAdmin} <Loader.Item height="40px" />
/> <Loader.Item height="40px" />
) </Loader>
) : ( )}
<Loader className="space-y-5"> </div>
<Loader.Item height="40px" /> );
<Loader.Item height="40px" /> };
<Loader.Item height="40px" />
<Loader.Item height="40px" /> ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
</Loader> return (
)} <AppLayout withProjectWrapper header={<ProjectSettingHeader title="Integrations Settings" />}>
</div> <ProjectSettingLayout>{page}</ProjectSettingLayout>
</ProjectSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default ProjectIntegrations; export default ProjectIntegrationsPage;

View File

@ -1,5 +1,4 @@
import React from "react"; import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -7,16 +6,20 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { ProjectSettingsLabelList } from "components/labels"; import { ProjectSettingsLabelList } from "components/labels";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const LabelsSettings: NextPage = () => ( const LabelsSettingsPage: NextPageWithLayout = () => (
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="Labels Settings" />}> <div className="pr-9 py-8 gap-10 w-full overflow-y-auto">
<ProjectSettingLayout> <ProjectSettingsLabelList />
<div className="pr-9 py-8 gap-10 w-full overflow-y-auto"> </div>
<ProjectSettingsLabelList />
</div>
</ProjectSettingLayout>
</AppLayout>
); );
export default LabelsSettings; LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="Labels Settings" />}>
<ProjectSettingLayout>{page}</ProjectSettingLayout>
</AppLayout>
);
};
export default LabelsSettingsPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -5,17 +6,21 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const MembersSettings: NextPage = () => ( const MembersSettingsPage: NextPageWithLayout = () => (
<AppLayout header={<ProjectSettingHeader title="Members Settings" />} withProjectWrapper> <section className={`pr-9 py-8 w-full overflow-y-auto`}>
<ProjectSettingLayout> <ProjectSettingsMemberDefaults />
<section className={`pr-9 py-8 w-full overflow-y-auto`}> <ProjectMemberList />
<ProjectSettingsMemberDefaults /> </section>
<ProjectMemberList />
</section>
</ProjectSettingLayout>
</AppLayout>
); );
export default MembersSettings; MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectSettingHeader title="Members Settings" />} withProjectWrapper>
<ProjectSettingLayout>{page}</ProjectSettingLayout>
</AppLayout>
);
};
export default MembersSettingsPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
// layout // layout
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -6,20 +6,23 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { ProjectSettingStateList } from "components/states"; import { ProjectSettingStateList } from "components/states";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const StatesSettings: NextPage = () => ( const StatesSettingsPage: NextPageWithLayout = () => (
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="States Settings" />}> <div className="pr-9 py-8 gap-10 w-full overflow-y-auto">
<ProjectSettingLayout> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<div className="pr-9 py-8 gap-10 w-full overflow-y-auto"> <h3 className="text-xl font-medium">States</h3>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> </div>
<h3 className="text-xl font-medium">States</h3> <ProjectSettingStateList />
</div> </div>
<ProjectSettingStateList />
</div>
</ProjectSettingLayout>
</AppLayout>
); );
export default StatesSettings; StatesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="States Settings" />}>
<ProjectSettingLayout>{page}</ProjectSettingLayout>
</AppLayout>
);
};
export default StatesSettingsPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
@ -12,9 +13,9 @@ import { EmptyState } from "components/common";
// assets // assets
import emptyView from "public/empty-state/view.svg"; import emptyView from "public/empty-state/view.svg";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ProjectViewIssues: NextPage = () => { const ProjectViewIssuesPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query;
@ -28,7 +29,7 @@ const ProjectViewIssues: NextPage = () => {
); );
return ( return (
<AppLayout header={<ProjectViewIssuesHeader />} withProjectWrapper> <>
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyView} image={emptyView}
@ -42,8 +43,16 @@ const ProjectViewIssues: NextPage = () => {
) : ( ) : (
<ProjectViewLayoutRoot /> <ProjectViewLayoutRoot />
)} )}
</>
);
};
ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<ProjectViewIssuesHeader />} withProjectWrapper>
{page}
</AppLayout> </AppLayout>
); );
}; };
export default ProjectViewIssues; export default ProjectViewIssuesPage;

View File

@ -1,5 +1,4 @@
import React from "react"; import { ReactElement } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
@ -9,25 +8,31 @@ import { ProjectViewsHeader } from "components/headers";
import { ProjectViewsList } from "components/views"; import { ProjectViewsList } from "components/views";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types
import { NextPageWithLayout } from "types/app";
const ProjectViews: NextPage = () => { const ProjectViewsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store
const { projectViews: projectViewsStore } = useMobxStore(); const {
projectViews: { fetchAllViews },
} = useMobxStore();
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_VIEWS_LIST_${workspaceSlug.toString()}_${projectId.toString()}` : null, workspaceSlug && projectId ? `PROJECT_VIEWS_LIST_${workspaceSlug.toString()}_${projectId.toString()}` : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null
? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString())
: null
); );
return <ProjectViewsList />;
};
ProjectViewsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<ProjectViewsHeader />} withProjectWrapper> <AppLayout header={<ProjectViewsHeader />} withProjectWrapper>
<ProjectViewsList /> {page}
</AppLayout> </AppLayout>
); );
}; };
export default ProjectViews; export default ProjectViewsPage;

View File

@ -1,21 +1,23 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type { NextPage } from "next";
// components // components
import { ProjectCardList } from "components/project"; import { ProjectCardList } from "components/project";
import { ProjectsHeader } from "components/headers"; import { ProjectsHeader } from "components/headers";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// type
import { NextPageWithLayout } from "types/app";
const ProjectsPage: NextPage = () => { const ProjectsPage: NextPageWithLayout = () => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
return ( return <>{workspaceSlug && <ProjectCardList workspaceSlug={workspaceSlug.toString()} />}</>;
<AppLayout header={<ProjectsHeader />}> };
<>{workspaceSlug && <ProjectCardList workspaceSlug={workspaceSlug.toString()} />}</>
</AppLayout> ProjectsPage.getLayout = function getLayout(page: ReactElement) {
); return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;
}; };
export default ProjectsPage; export default ProjectsPage;

View File

@ -1,4 +1,4 @@
import React from "react"; import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
@ -7,29 +7,33 @@ import { WorkspaceSettingHeader } from "components/headers";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const BillingSettings: NextPage = () => ( const BillingSettingsPage: NextPageWithLayout = () => (
<AppLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}> <section className="pr-9 py-8 w-full overflow-y-auto">
<WorkspaceSettingLayout> <div>
<section className="pr-9 py-8 w-full overflow-y-auto"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<div> <h3 className="text-xl font-medium">Billing & Plans</h3>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> </div>
<h3 className="text-xl font-medium">Billing & Plans</h3> </div>
</div> <div className="px-4 py-6">
</div> <div>
<div className="px-4 py-6"> <h4 className="text-md mb-1 leading-6">Current plan</h4>
<div> <p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
<h4 className="text-md mb-1 leading-6">Current plan</h4> <a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p> <Button variant="neutral-primary">View Plans</Button>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer"> </a>
<Button variant="neutral-primary">View Plans</Button> </div>
</a> </div>
</div> </section>
</div>
</section>
</WorkspaceSettingLayout>
</AppLayout>
); );
export default BillingSettings; BillingSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout>
);
};
export default BillingSettingsPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// layout // layout
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
@ -5,10 +6,9 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import ExportGuide from "components/exporter/guide"; import ExportGuide from "components/exporter/guide";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// helper
const ImportExport: NextPage = () => ( const ExportsPage: NextPageWithLayout = () => (
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}> <AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
<WorkspaceSettingLayout> <WorkspaceSettingLayout>
<div className="pr-9 py-8 w-full overflow-y-auto"> <div className="pr-9 py-8 w-full overflow-y-auto">
@ -21,4 +21,12 @@ const ImportExport: NextPage = () => (
</AppLayout> </AppLayout>
); );
export default ImportExport; ExportsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout>
);
};
export default ExportsPage;

View File

@ -1,23 +1,28 @@
import { ReactElement } from "react";
// layouts // layouts
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const ImportExport: NextPage = () => ( const ImportsPage: NextPageWithLayout = () => (
<AppLayout header={<WorkspaceSettingHeader title="Import Settings" />}> <section className="pr-9 py-8 w-full overflow-y-auto">
<WorkspaceSettingLayout> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<section className="pr-9 py-8 w-full overflow-y-auto"> <h3 className="text-xl font-medium">Imports</h3>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> </div>
<h3 className="text-xl font-medium">Imports</h3> <IntegrationGuide />
</div> </section>
<IntegrationGuide />
</section>
</WorkspaceSettingLayout>
</AppLayout>
); );
export default ImportExport; ImportsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="Import Settings" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout>
);
};
export default ImportsPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
@ -5,14 +6,16 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { WorkspaceDetails } from "components/workspace"; import { WorkspaceDetails } from "components/workspace";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const WorkspaceSettings: NextPage = () => ( const WorkspaceSettingsPage: NextPageWithLayout = () => <WorkspaceDetails />;
<AppLayout header={<WorkspaceSettingHeader title="General Settings" />}>
<WorkspaceSettingLayout>
<WorkspaceDetails />
</WorkspaceSettingLayout>
</AppLayout>
);
export default WorkspaceSettings; WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="General Settings" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout>
);
};
export default WorkspaceSettingsPage;

View File

@ -1,9 +1,6 @@
import React from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services // services
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
// layouts // layouts
@ -12,19 +9,18 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components // components
import { SingleIntegrationCard } from "components/integration"; import { SingleIntegrationCard } from "components/integration";
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { Loader } from "@plane/ui";
// ui // ui
import { IntegrationAndImportExportBanner } from "components/ui"; import { IntegrationAndImportExportBanner } from "components/ui";
import { Loader } from "@plane/ui";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// fetch-keys // fetch-keys
import { APP_INTEGRATIONS } from "constants/fetch-keys"; import { APP_INTEGRATIONS } from "constants/fetch-keys";
// helper
// services // services
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
const WorkspaceIntegrations: NextPage = () => { const WorkspaceIntegrationsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -32,27 +28,29 @@ const WorkspaceIntegrations: NextPage = () => {
workspaceSlug ? integrationService.getAppIntegrationsList() : null workspaceSlug ? integrationService.getAppIntegrationsList() : null
); );
return (
<section className="pr-9 py-8 w-full overflow-y-auto">
<IntegrationAndImportExportBanner bannerName="Integrations" />
<div>
{appIntegrations ? (
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />)
) : (
<Loader className="space-y-2.5 mt-4">
<Loader.Item height="89px" />
<Loader.Item height="89px" />
</Loader>
)}
</div>
</section>
);
};
WorkspaceIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}> <AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
<WorkspaceSettingLayout> <WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
<section className="pr-9 py-8 w-full overflow-y-auto">
<IntegrationAndImportExportBanner bannerName="Integrations" />
<div>
{appIntegrations ? (
appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} />
))
) : (
<Loader className="space-y-2.5 mt-4">
<Loader.Item height="89px" />
<Loader.Item height="89px" />
</Loader>
)}
</div>
</section>
</WorkspaceSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default WorkspaceIntegrations; export default WorkspaceIntegrationsPage;

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
@ -10,52 +10,59 @@ import { WorkspaceSettingHeader } from "components/headers";
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types
import type { NextPage } from "next";
// icons // icons
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// types
import { NextPageWithLayout } from "types/app";
const MembersSettings: NextPage = () => { const WorkspaceMembersSettingsPage: NextPageWithLayout = () => {
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// states
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
// hooks
const { user } = useUser(); const { user } = useUser();
return ( return (
<AppLayout header={<WorkspaceSettingHeader title="Members Settings" />}> <>
<WorkspaceSettingLayout> {workspaceSlug && (
{workspaceSlug && ( <SendWorkspaceInvitationModal
<SendWorkspaceInvitationModal isOpen={inviteModal}
isOpen={inviteModal} onClose={() => setInviteModal(false)}
onClose={() => setInviteModal(false)} workspaceSlug={workspaceSlug.toString()}
workspaceSlug={workspaceSlug.toString()} user={user}
user={user} />
/> )}
)} <section className="pr-9 py-8 w-full overflow-y-auto">
<section className="pr-9 py-8 w-full overflow-y-auto"> <div className="flex items-center justify-between gap-4 py-3.5 border-b-[0.5px] border-custom-border-200">
<div className="flex items-center justify-between gap-4 py-3.5 border-b-[0.5px] border-custom-border-200"> <h4 className="text-xl font-medium">Members</h4>
<h4 className="text-xl font-medium">Members</h4> <div className="flex gap-1 items-center justify-start ml-auto text-custom-text-400 rounded-md px-2.5 py-1.5 border border-custom-border-200 bg-custom-background-100">
<div className="flex gap-1 items-center justify-start ml-auto text-custom-text-400 rounded-md px-2.5 py-1.5 border border-custom-border-200 bg-custom-background-100"> <Search className="h-3.5 w-3.5" />
<Search className="h-3.5 w-3.5" /> <input
<input className="max-w-[234px] w-full border-none bg-transparent text-sm focus:outline-none"
className="max-w-[234px] w-full border-none bg-transparent text-sm focus:outline-none" placeholder="Search"
placeholder="Search" value={searchQuery}
value={searchQuery} autoFocus={true}
autoFocus={true} onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => setSearchQuery(e.target.value)} />
/>
</div>
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
Add Member
</Button>
</div> </div>
<WorkspaceMembersList searchQuery={searchQuery} /> <Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
</section> Add Member
</WorkspaceSettingLayout> </Button>
</div>
<WorkspaceMembersList searchQuery={searchQuery} />
</section>
</>
);
};
WorkspaceMembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout header={<WorkspaceSettingHeader title="Members Settings" />}>
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
</AppLayout> </AppLayout>
); );
}; };
export default MembersSettings; export default WorkspaceMembersSettingsPage;

View File

@ -1,6 +1,6 @@
import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// layouts // layouts
@ -10,31 +10,35 @@ import { GlobalViewsHeader } from "components/workspace";
import { GlobalViewLayoutRoot } from "components/issues"; import { GlobalViewLayoutRoot } from "components/issues";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const GlobalViewIssues: NextPage = () => { const GlobalViewIssuesPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, globalViewId } = router.query; const { workspaceSlug, globalViewId } = router.query;
const { globalViews: globalViewsStore } = useMobxStore(); const {
globalViews: { fetchGlobalViewDetails },
} = useMobxStore();
useSWR( useSWR(
workspaceSlug && globalViewId ? `GLOBAL_VIEW_DETAILS_${globalViewId.toString()}` : null, workspaceSlug && globalViewId ? `GLOBAL_VIEW_DETAILS_${globalViewId.toString()}` : null,
workspaceSlug && globalViewId workspaceSlug && globalViewId
? () => globalViewsStore.fetchGlobalViewDetails(workspaceSlug.toString(), globalViewId.toString()) ? () => fetchGlobalViewDetails(workspaceSlug.toString(), globalViewId.toString())
: null : null
); );
return ( return (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full w-full flex flex-col border-b border-custom-border-300">
<div className="h-full w-full flex flex-col border-b border-custom-border-300"> <GlobalViewsHeader />
<GlobalViewsHeader /> <GlobalViewLayoutRoot />
<GlobalViewLayoutRoot />
</div>
</div> </div>
</AppLayout> </div>
); );
}; };
export default GlobalViewIssues; GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewIssuesPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// components // components
import { GlobalViewsHeader } from "components/workspace"; import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
@ -5,17 +6,19 @@ import { GlobalViewLayoutRoot } from "components/issues";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const GlobalViewAllIssues: NextPage = () => ( const GlobalViewAllIssuesPage: NextPageWithLayout = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full w-full flex flex-col border-b border-custom-border-300">
<div className="h-full w-full flex flex-col border-b border-custom-border-300"> <GlobalViewsHeader />
<GlobalViewsHeader /> <GlobalViewLayoutRoot type="all-issues" />
<GlobalViewLayoutRoot type="all-issues" />
</div>
</div> </div>
</AppLayout> </div>
); );
export default GlobalViewAllIssues; GlobalViewAllIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewAllIssuesPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// components // components
import { GlobalViewsHeader } from "components/workspace"; import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
@ -5,17 +6,19 @@ import { GlobalViewLayoutRoot } from "components/issues";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const GlobalViewAssignedIssues: NextPage = () => ( const GlobalViewAssignedIssuesPage: NextPageWithLayout = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full w-full flex flex-col border-b border-custom-border-300">
<div className="h-full w-full flex flex-col border-b border-custom-border-300"> <GlobalViewsHeader />
<GlobalViewsHeader /> <GlobalViewLayoutRoot type="assigned" />
<GlobalViewLayoutRoot type="assigned" />
</div>
</div> </div>
</AppLayout> </div>
); );
export default GlobalViewAssignedIssues; GlobalViewAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewAssignedIssuesPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// components // components
import { GlobalViewsHeader } from "components/workspace"; import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
@ -5,17 +6,19 @@ import { GlobalViewLayoutRoot } from "components/issues";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const GlobalViewCreatedIssues: NextPage = () => ( const GlobalViewCreatedIssuesPage: NextPageWithLayout = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full w-full flex flex-col border-b border-custom-border-300">
<div className="h-full w-full flex flex-col border-b border-custom-border-300"> <GlobalViewsHeader />
<GlobalViewsHeader /> <GlobalViewLayoutRoot type="created" />
<GlobalViewLayoutRoot type="created" />
</div>
</div> </div>
</AppLayout> </div>
); );
export default GlobalViewCreatedIssues; GlobalViewCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewCreatedIssuesPage;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
@ -9,35 +9,37 @@ import { Input } from "@plane/ui";
// icons // icons
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// constants // constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
const WorkspaceViews: NextPage = () => { const WorkspaceViewsPage: NextPageWithLayout = () => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
return ( return (
<AppLayout header={<GlobalIssuesHeader activeLayout="list" />}> <div className="flex flex-col">
<div className="flex flex-col"> <div className="h-full w-full flex flex-col overflow-hidden">
<div className="h-full w-full flex flex-col overflow-hidden"> <div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200">
<div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200"> <Search className="text-custom-text-200" size={14} strokeWidth={2} />
<Search className="text-custom-text-200" size={14} strokeWidth={2} /> <Input
<Input className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none"
className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none" value={query}
value={query} onChange={(e) => setQuery(e.target.value)}
onChange={(e) => setQuery(e.target.value)} placeholder="Search"
placeholder="Search" mode="true-transparent"
mode="true-transparent" />
/>
</div>
</div> </div>
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div> </div>
</AppLayout> {DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div>
); );
}; };
export default WorkspaceViews; WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;
};
export default WorkspaceViewsPage;

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
@ -5,17 +6,19 @@ import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewLayoutRoot } from "components/issues"; import { GlobalViewLayoutRoot } from "components/issues";
// types // types
import { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const GlobalViewSubscribedIssues: NextPage = () => ( const GlobalViewSubscribedIssuesPage: NextPageWithLayout = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full w-full flex flex-col border-b border-custom-border-300">
<div className="h-full w-full flex flex-col border-b border-custom-border-300"> <GlobalViewsHeader />
<GlobalViewsHeader /> <GlobalViewLayoutRoot type="subscribed" />
<GlobalViewLayoutRoot type="subscribed" />
</div>
</div> </div>
</AppLayout> </div>
); );
export default GlobalViewSubscribedIssues; GlobalViewSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewSubscribedIssuesPage;

View File

@ -1,6 +1,8 @@
import { ReactElement } from "react";
import Head from "next/head"; import Head from "next/head";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Router from "next/router"; import Router from "next/router";
import { AppProps } from "next/app";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import NProgress from "nprogress"; import NProgress from "nprogress";
// styles // styles
@ -11,15 +13,14 @@ import "styles/nprogress.css";
import "styles/react-datepicker.css"; import "styles/react-datepicker.css";
// contexts // contexts
import { ToastContextProvider } from "contexts/toast.context"; import { ToastContextProvider } from "contexts/toast.context";
// types
import type { AppProps } from "next/app";
// constants // constants
import { THEMES } from "constants/themes"; import { THEMES } from "constants/themes";
// constants
import { SITE_TITLE } from "constants/seo-variables"; import { SITE_TITLE } from "constants/seo-variables";
// mobx store provider // mobx store provider
import { MobxStoreProvider } from "lib/mobx/store-provider"; import { MobxStoreProvider } from "lib/mobx/store-provider";
import MobxStoreInit from "lib/mobx/store-init"; import MobxStoreInit from "lib/mobx/store-init";
// types
import { NextPageWithLayout } from "types/app";
const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false }); const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false });
@ -29,7 +30,14 @@ Router.events.on("routeChangeStart", NProgress.start);
Router.events.on("routeChangeError", NProgress.done); Router.events.on("routeChangeError", NProgress.done);
Router.events.on("routeChangeComplete", NProgress.done); Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) { type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page: ReactElement) => page);
return ( return (
<> <>
<Head> <Head>
@ -40,7 +48,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<ToastContextProvider> <ToastContextProvider>
<CrispWithNoSSR /> <CrispWithNoSSR />
<MobxStoreInit /> <MobxStoreInit />
<Component {...pageProps} /> {getLayout(<Component {...pageProps} />)}
</ToastContextProvider> </ToastContextProvider>
</ThemeProvider> </ThemeProvider>
</MobxStoreProvider> </MobxStoreProvider>

View File

@ -1,4 +1,4 @@
import { NextPage } from "next"; import { ReactElement } from "react";
import Image from "next/image"; import Image from "next/image";
// components // components
import { EmailForgotPasswordForm, EmailForgotPasswordFormValues } from "components/account"; import { EmailForgotPasswordForm, EmailForgotPasswordFormValues } from "components/account";
@ -10,10 +10,12 @@ import { UserService } from "services/user.service";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types
import { NextPageWithLayout } from "types/app";
const userService = new UserService(); const userService = new UserService();
const ForgotPasswordPage: NextPage = () => { const ForgotPasswordPage: NextPageWithLayout = () => {
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -47,25 +49,27 @@ const ForgotPasswordPage: NextPage = () => {
}); });
}; };
return ( return (
<DefaultLayout> <>
<> <div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" /> <div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28"> <div className="grid place-items-center bg-custom-background-100">
<div className="grid place-items-center bg-custom-background-100"> <div className="h-[30px] w-[30px]">
<div className="h-[30px] w-[30px]"> <Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div> </div>
</div> </div>
</> </div>
<div className="grid place-items-center h-full overflow-y-auto py-6 px-7"> <div className="grid place-items-center h-full overflow-y-auto py-6 px-7">
<div> <div>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Forgot Password</h1> <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Forgot Password</h1>
<EmailForgotPasswordForm onSubmit={handleForgotPassword} /> <EmailForgotPasswordForm onSubmit={handleForgotPassword} />
</div> </div>
</div> </div>
</DefaultLayout> </>
); );
}; };
ForgotPasswordPage.getLayout = function getLayout(page: ReactElement) {
return <DefaultLayout>{page}</DefaultLayout>;
};
export default ForgotPasswordPage; export default ForgotPasswordPage;

View File

@ -1,8 +1,5 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
@ -12,11 +9,11 @@ import { AuthService } from "services/auth.service";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const authService = new AuthService(); const authService = new AuthService();
const MagicSignIn: NextPage = () => { const MagicSignInPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { password, key } = router.query; const { password, key } = router.query;
@ -55,54 +52,54 @@ const MagicSignIn: NextPage = () => {
}, [password, key, mutateUser, router]); }, [password, key, mutateUser, router]);
return ( return (
<DefaultLayout> <div className="h-screen w-full overflow-auto bg-custom-background-90">
<div className="h-screen w-full overflow-auto bg-custom-background-90"> {isSigningIn ? (
{isSigningIn ? ( <div className="flex h-full w-full flex-col items-center justify-center gap-3">
<div className="flex h-full w-full flex-col items-center justify-center gap-3"> <h2 className="text-4xl font-medium">Signing you in...</h2>
<h2 className="text-4xl font-medium">Signing you in...</h2> <p className="text-sm font-medium text-custom-text-200">Please wait while we are preparing your take off.</p>
<p className="text-sm font-medium text-custom-text-200"> </div>
Please wait while we are preparing your take off. ) : errorSigningIn ? (
</p> <div className="flex h-full w-full flex-col items-center justify-center gap-3">
</div> <h2 className="text-4xl font-medium">Error</h2>
) : errorSigningIn ? ( <div className="text-sm font-medium text-custom-text-200 flex gap-2">
<div className="flex h-full w-full flex-col items-center justify-center gap-3"> <div>{errorSigningIn}.</div>
<h2 className="text-4xl font-medium">Error</h2> <span
<div className="text-sm font-medium text-custom-text-200 flex gap-2"> className="cursor-pointer underline"
<div>{errorSigningIn}.</div> onClick={() => {
<span authService
className="cursor-pointer underline" .emailCode({ email: (key as string).split("_")[1] })
onClick={() => { .then(() => {
authService setToastAlert({
.emailCode({ email: (key as string).split("_")[1] }) type: "success",
.then(() => { title: "Email sent",
setToastAlert({ message: "A new link/code has been send to you.",
type: "success",
title: "Email sent",
message: "A new link/code has been send to you.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error",
message: "Unable to send email.",
});
}); });
}} })
> .catch(() => {
Send link again? setToastAlert({
</span> type: "error",
</div> title: "Error",
message: "Unable to send email.",
});
});
}}
>
Send link again?
</span>
</div> </div>
) : ( </div>
<div className="flex h-full w-full flex-col items-center justify-center gap-y-2"> ) : (
<h2 className="text-4xl font-medium">Success</h2> <div className="flex h-full w-full flex-col items-center justify-center gap-y-2">
<p className="text-sm font-medium text-custom-text-200">Redirecting you to the app...</p> <h2 className="text-4xl font-medium">Success</h2>
</div> <p className="text-sm font-medium text-custom-text-200">Redirecting you to the app...</p>
)} </div>
</div> )}
</DefaultLayout> </div>
); );
}; };
export default MagicSignIn; MagicSignInPage.getLayout = function getLayout(page: ReactElement) {
return <DefaultLayout>{page}</DefaultLayout>;
};
export default MagicSignInPage;

View File

@ -1,11 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Image from "next/image"; import Image from "next/image";
// next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -18,7 +14,7 @@ import { Button, Input, Spinner } from "@plane/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
type FormData = { type FormData = {
password: string; password: string;
@ -28,7 +24,7 @@ type FormData = {
// services // services
const userService = new UserService(); const userService = new UserService();
const ResetPasswordPage: NextPage = () => { const ResetPasswordPage: NextPageWithLayout = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const router = useRouter(); const router = useRouter();
@ -98,21 +94,18 @@ const ResetPasswordPage: NextPage = () => {
); );
return ( return (
<DefaultLayout> <>
<> <div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" /> <div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28"> <div className="grid place-items-center bg-custom-background-100">
<div className="grid place-items-center bg-custom-background-100"> <div className="h-[30px] w-[30px]">
<div className="h-[30px] w-[30px]"> <Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div> </div>
</div> </div>
</> </div>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div className="w-full"> <div className="w-full">
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Reset your password</h1> <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Reset your password</h1>
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}> <form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-1"> <div className="space-y-1">
<Controller <Controller
@ -164,8 +157,12 @@ const ResetPasswordPage: NextPage = () => {
</form> </form>
</div> </div>
</div> </div>
</DefaultLayout> </>
); );
}; };
ResetPasswordPage.getLayout = function getLayout(page: ReactElement) {
return <DefaultLayout>{page}</DefaultLayout>;
};
export default ResetPasswordPage; export default ResetPasswordPage;

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React, { useEffect, ReactElement } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes // next-themes
@ -15,7 +15,8 @@ import { EmailSignUpForm } from "components/account";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
type EmailPasswordFormValues = { type EmailPasswordFormValues = {
email: string; email: string;
password?: string; password?: string;
@ -25,7 +26,7 @@ type EmailPasswordFormValues = {
// services // services
const authService = new AuthService(); const authService = new AuthService();
const SignUp: NextPage = () => { const SignUpPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -66,25 +67,27 @@ const SignUp: NextPage = () => {
}, [setTheme]); }, [setTheme]);
return ( return (
<DefaultLayout> <>
<> <div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" /> <div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28"> <div className="grid place-items-center bg-custom-background-100">
<div className="grid place-items-center bg-custom-background-100"> <div className="h-[30px] w-[30px]">
<div className="h-[30px] w-[30px]"> <Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div> </div>
</div> </div>
</> </div>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div> <div>
<h1 className="text-2xl text-center font-">SignUp on Plane</h1> <h1 className="text-2xl text-center font-">SignUp on Plane</h1>
<EmailSignUpForm onSubmit={handleSignUp} /> <EmailSignUpForm onSubmit={handleSignUp} />
</div> </div>
</div> </div>
</DefaultLayout> </>
); );
}; };
export default SignUp; SignUpPage.getLayout = function getLayout(page: ReactElement) {
return <DefaultLayout>{page}</DefaultLayout>;
};
export default SignUpPage;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@ -15,9 +15,9 @@ import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-l
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types // types
import { IWorkspace } from "types"; import { IWorkspace } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
const CreateWorkspace: NextPage = observer(() => { const CreateWorkspacePage: NextPageWithLayout = observer(() => {
const [defaultValues, setDefaultValues] = useState({ const [defaultValues, setDefaultValues] = useState({
name: "", name: "",
slug: "", slug: "",
@ -38,43 +38,47 @@ const CreateWorkspace: NextPage = observer(() => {
}; };
return ( return (
<UserAuthWrapper> <div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<DefaultLayout> <div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden"> <div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" />
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5"> <button
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" /> className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12"
<button onClick={() => router.push("/")}
className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12" >
onClick={() => router.push("/")} <div className="h-[30px] w-[133px]">
> {theme === "light" ? (
<div className="h-[30px] w-[133px]"> <Image src={BlackHorizontalLogo} alt="Plane black logo" />
{theme === "light" ? ( ) : (
<Image src={BlackHorizontalLogo} alt="Plane black logo" /> <Image src={WhiteHorizontalLogo} alt="Plane white logo" />
) : ( )}
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
</button>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div> </div>
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5"> </button>
<div className="w-full space-y-7 sm:space-y-10"> <div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
<h4 className="text-2xl font-semibold">Create your workspace</h4> {user?.email}
<div className="sm:w-3/4 md:w-2/5"> </div>
<CreateWorkspaceForm </div>
onSubmit={onSubmit} <div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
defaultValues={defaultValues} <div className="w-full space-y-7 sm:space-y-10">
setDefaultValues={setDefaultValues} <h4 className="text-2xl font-semibold">Create your workspace</h4>
/> <div className="sm:w-3/4 md:w-2/5">
</div> <CreateWorkspaceForm
</div> onSubmit={onSubmit}
defaultValues={defaultValues}
setDefaultValues={setDefaultValues}
/>
</div> </div>
</div> </div>
</DefaultLayout> </div>
</UserAuthWrapper> </div>
); );
}); });
export default CreateWorkspace; CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<DefaultLayout>{page} </DefaultLayout>
</UserAuthWrapper>
);
};
export default CreateWorkspacePage;

View File

@ -1,13 +1,15 @@
import type { NextPage } from "next"; import { ReactElement } from "react";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { SignInView } from "components/page-views"; import { SignInView } from "components/page-views";
// type
import { NextPageWithLayout } from "types/app";
const HomePage: NextPage = () => ( const HomePage: NextPageWithLayout = () => <SignInView />;
<DefaultLayout>
<SignInView /> HomePage.getLayout = function getLayout(page: ReactElement) {
</DefaultLayout> return <DefaultLayout>{page}</DefaultLayout>;
); };
export default HomePage; export default HomePage;

View File

@ -1,16 +1,16 @@
import React, { useEffect } from "react"; import React, { useEffect, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// services // services
import { AppInstallationService } from "services/app_installation.service"; import { AppInstallationService } from "services/app_installation.service";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types
import { NextPageWithLayout } from "types/app";
// services // services
const appInstallationService = new AppInstallationService(); const appInstallationService = new AppInstallationService();
const AppPostInstallation = () => { const AppPostInstallation: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { installation_id, setup_action, state, provider, code } = router.query; const { installation_id, setup_action, state, provider, code } = router.query;
@ -85,4 +85,8 @@ const AppPostInstallation = () => {
); );
}; };
AppPostInstallation.getLayout = function getLayout(page: ReactElement) {
return <div>{page}</div>;
};
export default AppPostInstallation; export default AppPostInstallation;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, ReactElement } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -24,7 +24,7 @@ import emptyInvitation from "public/empty-state/invitation.svg";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
import type { IWorkspaceMemberInvitation } from "types"; import type { IWorkspaceMemberInvitation } from "types";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
@ -35,7 +35,7 @@ import { EmptyState } from "components/common";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const userService = new UserService(); const userService = new UserService();
const UserInvitationsPage: NextPage = () => { const UserInvitationsPage: NextPageWithLayout = () => {
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
@ -103,113 +103,115 @@ const UserInvitationsPage: NextPage = () => {
}; };
return ( return (
<UserAuthWrapper> <div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<DefaultLayout> <div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden"> <div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" />
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5"> <div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12">
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" /> <div className="h-[30px] w-[133px]">
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12"> {theme === "light" ? (
<div className="h-[30px] w-[133px]"> <Image src={BlackHorizontalLogo} alt="Plane black logo" />
{theme === "light" ? ( ) : (
<Image src={BlackHorizontalLogo} alt="Plane black logo" /> <Image src={WhiteHorizontalLogo} alt="Plane white logo" />
) : ( )}
<Image src={WhiteHorizontalLogo} alt="Plane white logo" /> </div>
)} </div>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div>
{invitations ? (
invitations.length > 0 ? (
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="max-h-[37vh] md:w-3/5 space-y-4 overflow-y-auto">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}
>
<CheckCircle2 className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<Button
variant="primary"
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
>
Accept & Join
</Button>
<Link href="/">
<a>
<Button variant="neutral-primary" size="md">
Go Home
</Button>
</a>
</Link>
</div> </div>
</div>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div> </div>
</div> </div>
{invitations ? ( ) : (
invitations.length > 0 ? ( <div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5"> <EmptyState
<div className="w-full space-y-10"> title="No pending invites"
<h5 className="text-lg">We see that someone has invited you to</h5> description="You can see here if someone invites you to a workspace."
<h4 className="text-2xl font-semibold">Join a workspace</h4> image={emptyInvitation}
<div className="max-h-[37vh] md:w-3/5 space-y-4 overflow-y-auto"> primaryButton={{
{invitations.map((invitation) => { text: "Back to Dashboard",
const isSelected = invitationsRespond.includes(invitation.id); onClick: () => router.push("/"),
}}
/>
</div>
)
) : null}
</div>
);
};
return ( UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
<div return (
key={invitation.id} <UserAuthWrapper>
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${ <DefaultLayout>{page}</DefaultLayout>
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
}`}
>
<CheckCircle2 className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<Button
variant="primary"
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
>
Accept & Join
</Button>
<Link href="/">
<a>
<Button variant="neutral-primary" size="md">
Go Home
</Button>
</a>
</Link>
</div>
</div>
</div>
) : (
<div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
primaryButton={{
text: "Back to Dashboard",
onClick: () => router.push("/"),
}}
/>
</div>
)
) : null}
</div>
</DefaultLayout>
</UserAuthWrapper> </UserAuthWrapper>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, ReactElement } from "react";
import Image from "next/image"; import Image from "next/image";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -22,12 +22,12 @@ import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-l
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types // types
import { IUser, TOnboardingSteps } from "types"; import { IUser, TOnboardingSteps } from "types";
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const Onboarding: NextPage = observer(() => { const OnboardingPage: NextPageWithLayout = observer(() => {
const [step, setStep] = useState<number | null>(null); const [step, setStep] = useState<number | null>(null);
const { user: userStore, workspace: workspaceStore } = useMobxStore(); const { user: userStore, workspace: workspaceStore } = useMobxStore();
@ -105,85 +105,91 @@ const Onboarding: NextPage = observer(() => {
}, [user, invitations, step]); }, [user, invitations, step]);
return ( return (
<UserAuthWrapper> <>
{user && step !== null ? ( {user && step !== null ? (
<DefaultLayout> <div className="flex h-full w-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<div className="flex h-full w-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden"> <div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5"> <div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" /> {step === 1 ? (
{step === 1 ? ( <div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10"> <div className="h-[30px] w-[30px]">
<div className="h-[30px] w-[30px]"> <Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
</div>
</div> </div>
) : (
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="h-[30px] w-[133px]">
{theme === "light" ? (
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
) : (
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
</div>
)}
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div> </div>
</div> ) : (
<div className="relative flex justify-center sm:items-center h-full px-8 pb-0 sm:px-0 sm:py-12 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5 overflow-hidden"> <div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
{step === 1 ? ( <div className="h-[30px] w-[133px]">
<UserDetails user={user} /> {theme === "light" ? (
) : step === 2 ? ( <Image src={BlackHorizontalLogo} alt="Plane black logo" />
<Workspace ) : (
finishOnboarding={finishOnboarding} <Image src={WhiteHorizontalLogo} alt="Plane white logo" />
stepChange={stepChange} )}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/>
) : step === 3 ? (
<InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={userWorkspaces?.[0]}
/>
) : (
step === 4 && (
<JoinWorkspaces
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
/>
)
)}
</div>
{step !== 4 && (
<div className="sticky sm:fixed bottom-0 md:bottom-14 md:right-16 py-6 md:py-0 flex justify-center md:justify-end bg-custom-background-100 md:bg-transparent pointer-events-none w-full z-[1]">
<div className="w-3/4 md:w-1/5 space-y-1">
<p className="text-xs text-custom-text-200">{step} of 3 steps</p>
<div className="relative h-1 w-full rounded bg-custom-background-80">
<div
className="absolute top-0 left-0 h-1 rounded bg-custom-primary-100 duration-300"
style={{
width: `${((step / 3) * 100).toFixed(0)}%`,
}}
/>
</div>
</div> </div>
</div> </div>
)} )}
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div> </div>
</DefaultLayout> <div className="relative flex justify-center sm:items-center h-full px-8 pb-0 sm:px-0 sm:py-12 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5 overflow-hidden">
{step === 1 ? (
<UserDetails user={user} />
) : step === 2 ? (
<Workspace
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/>
) : step === 3 ? (
<InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={userWorkspaces?.[0]}
/>
) : (
step === 4 && (
<JoinWorkspaces
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
/>
)
)}
</div>
{step !== 4 && (
<div className="sticky sm:fixed bottom-0 md:bottom-14 md:right-16 py-6 md:py-0 flex justify-center md:justify-end bg-custom-background-100 md:bg-transparent pointer-events-none w-full z-[1]">
<div className="w-3/4 md:w-1/5 space-y-1">
<p className="text-xs text-custom-text-200">{step} of 3 steps</p>
<div className="relative h-1 w-full rounded bg-custom-background-80">
<div
className="absolute top-0 left-0 h-1 rounded bg-custom-primary-100 duration-300"
style={{
width: `${((step / 3) * 100).toFixed(0)}%`,
}}
/>
</div>
</div>
</div>
)}
</div>
) : ( ) : (
<div className="h-screen w-full grid place-items-center"> <div className="h-screen w-full grid place-items-center">
<Spinner /> <Spinner />
</div> </div>
)} )}
</UserAuthWrapper> </>
); );
}); });
export default Onboarding; OnboardingPage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<DefaultLayout>{page}</DefaultLayout>
</UserAuthWrapper>
);
};
export default OnboardingPage;

View File

@ -1,6 +1,4 @@
import React from "react"; import React, { ReactElement } from "react";
// next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// swr // swr
@ -16,14 +14,14 @@ import { Spinner } from "@plane/ui";
// icons // icons
import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space";
// types // types
import type { NextPage } from "next"; import { NextPageWithLayout } from "types/app";
// constants // constants
import { WORKSPACE_INVITATION } from "constants/fetch-keys"; import { WORKSPACE_INVITATION } from "constants/fetch-keys";
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const WorkspaceInvitation: NextPage = () => { const WorkspaceInvitationPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { invitation_id, email } = router.query; const { invitation_id, email } = router.query;
@ -58,89 +56,91 @@ const WorkspaceInvitation: NextPage = () => {
}; };
return ( return (
<DefaultLayout> <div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex h-full w-full flex-col items-center justify-center px-3"> {invitationDetail ? (
{invitationDetail ? ( <>
<> {error ? (
{error ? ( <div className="flex w-full flex-col space-y-4 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-8 text-center shadow-2xl md:w-1/3">
<div className="flex w-full flex-col space-y-4 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-8 text-center shadow-2xl md:w-1/3"> <h2 className="text-xl uppercase">INVITATION NOT FOUND</h2>
<h2 className="text-xl uppercase">INVITATION NOT FOUND</h2> </div>
</div> ) : (
) : ( <>
<> {invitationDetail.accepted ? (
{invitationDetail.accepted ? ( <>
<>
<EmptySpace
title={`You are already a member of ${invitationDetail.workspace.name}`}
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={Boxes} title="Continue to Dashboard" action={() => router.push("/")} />
</EmptySpace>
</>
) : (
<EmptySpace <EmptySpace
title={`You have been invited to ${invitationDetail.workspace.name}`} title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account." 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={Check} title="Accept" action={handleAccept} /> <EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" action={() => router.push("/")} />
<EmptySpaceItem
Icon={X}
title="Ignore"
action={() => {
router.push("/");
}}
/>
</EmptySpace> </EmptySpace>
)} </>
</> ) : (
)} <EmptySpace
</> title={`You have been invited to ${invitationDetail.workspace.name}`}
) : error ? ( description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
<EmptySpace >
title="This invitation link is not active anymore." <EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account." <EmptySpaceItem
link={{ text: "Or start from an empty project", href: "/" }} Icon={X}
> title="Ignore"
{!user ? ( action={() => {
<EmptySpaceItem router.push("/");
Icon={User2} }}
title="Sign in to continue" />
action={() => { </EmptySpace>
router.push("/"); )}
}} </>
/> )}
) : ( </>
<EmptySpaceItem ) : error ? (
Icon={Boxes} <EmptySpace
title="Continue to Dashboard" title="This invitation link is not active anymore."
action={() => { description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
router.push("/"); link={{ text: "Or start from an empty project", href: "/" }}
}} >
/> {!user ? (
)}
<EmptySpaceItem <EmptySpaceItem
Icon={Star} Icon={User2}
title="Star us on GitHub" title="Sign in to continue"
action={() => { action={() => {
router.push("https://github.com/makeplane"); router.push("/");
}} }}
/> />
) : (
<EmptySpaceItem <EmptySpaceItem
Icon={Share2} Icon={Boxes}
title="Join our community of active creators" title="Continue to Dashboard"
action={() => { action={() => {
router.push("https://discord.com/invite/8SR2N9PAcJ"); router.push("/");
}} }}
/> />
</EmptySpace> )}
) : ( <EmptySpaceItem
<div className="flex h-full w-full items-center justify-center"> Icon={Star}
<Spinner /> title="Star us on GitHub"
</div> action={() => {
)} router.push("https://github.com/makeplane");
</div> }}
</DefaultLayout> />
<EmptySpaceItem
Icon={Share2}
title="Join our community of active creators"
action={() => {
router.push("https://discord.com/invite/8SR2N9PAcJ");
}}
/>
</EmptySpace>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</div>
); );
}; };
export default WorkspaceInvitation; WorkspaceInvitationPage.getLayout = function getLayout(page: ReactElement) {
return <DefaultLayout>{page}</DefaultLayout>;
};
export default WorkspaceInvitationPage;

3
web/types/app.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};