mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
a582021f2c
commit
3c884fd46e
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
3
web/types/app.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user