Merge branch 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-02-20 13:44:11 +05:30
commit a6a28d46c7
64 changed files with 1704 additions and 1223 deletions

View File

@ -4,3 +4,4 @@ export * from "./sidebar";
export * from "./theme"; export * from "./theme";
export * from "./activity"; export * from "./activity";
export * from "./image-picker-popover"; export * from "./image-picker-popover";
export * from "./page-title";

View File

@ -0,0 +1,18 @@
import Head from "next/head";
type PageHeadTitleProps = {
title?: string;
description?: string;
};
export const PageHead: React.FC<PageHeadTitleProps> = (props) => {
const { title } = props;
if (!title) return null;
return (
<Head>
<title>{title}</title>
</Head>
);
};

View File

@ -96,7 +96,7 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline" className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline"
> >
Your projects Recent projects
</Link> </Link>
<div className="space-y-8 mt-4 mx-7"> <div className="space-y-8 mt-4 mx-7">
{canCreateProject && ( {canCreateProject && (

View File

@ -288,7 +288,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
enableQuickIssueCreate={enableQuickAdd} enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
quickAddCallback={issues?.quickAddIssue} quickAddCallback={issues?.quickAddIssue}
viewId={viewId} viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}

View File

@ -48,6 +48,7 @@ export interface IGroupByKanBan {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
showEmptyGroup?: boolean;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -72,6 +73,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup = true,
} = props; } = props;
const member = useMember(); const member = useMember();
@ -84,6 +86,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null; if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0);
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) => const visibilityGroupBy = (_list: IGroupByColumn) =>
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false; sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
@ -91,9 +97,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
return ( return (
<div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}> <div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}>
{list && {groupList &&
list.length > 0 && groupList.length > 0 &&
list.map((_list: IGroupByColumn) => { groupList.map((_list: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list); const groupByVisibilityToggle = visibilityGroupBy(_list);
return ( return (
@ -196,6 +202,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup,
} = props; } = props;
const issueKanBanView = useKanbanView(); const issueKanBanView = useKanbanView();
@ -222,6 +229,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup}
/> />
); );
}); });

View File

@ -6,6 +6,7 @@ import { useApplication, useUser } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection"; import useSignInRedirection from "hooks/use-sign-in-redirection";
// components // components
import { SignInRoot } from "components/account"; import { SignInRoot } from "components/account";
import { PageHead } from "components/core";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// images // images
@ -34,6 +35,8 @@ export const SignInView = observer(() => {
); );
return ( return (
<>
<PageHead title="Sign In" />
<div className="h-full w-full bg-onboarding-gradient-100"> <div className="h-full w-full bg-onboarding-gradient-100">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28"> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="flex items-center gap-x-2 py-10"> <div className="flex items-center gap-x-2 py-10">
@ -48,5 +51,6 @@ export const SignInView = observer(() => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}); });

View File

@ -2,7 +2,7 @@ import { useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChevronUp, PenSquare, Search } from "lucide-react"; import { ChevronUp, PenSquare, Search } from "lucide-react";
// hooks // hooks
import { useApplication, useEventTracker, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { CreateUpdateDraftIssueModal } from "components/issues"; import { CreateUpdateDraftIssueModal } from "components/issues";
@ -16,6 +16,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
const { theme: themeStore, commandPalette: commandPaletteStore } = useApplication(); const { theme: themeStore, commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { joinedProjectIds } = useProject();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
@ -31,6 +32,8 @@ export const WorkspaceSidebarQuickAction = observer(() => {
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const disabled = joinedProjectIds.length === 0;
const onMouseEnter = () => { const onMouseEnter = () => {
//if renet before timout clear the timeout //if renet before timout clear the timeout
timeoutRef?.current && clearTimeout(timeoutRef.current); timeoutRef?.current && clearTimeout(timeoutRef.current);
@ -73,17 +76,18 @@ export const WorkspaceSidebarQuickAction = observer(() => {
type="button" type="button"
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${ className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
isSidebarCollapsed ? "justify-center" : "" isSidebarCollapsed ? "justify-center" : ""
}`} } ${disabled ? "cursor-not-allowed opacity-50" : ""}`}
onClick={() => { onClick={() => {
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS"); setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}} }}
disabled={disabled}
> >
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" /> <PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>} {!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button> </button>
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( {!disabled && storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<> <>
<div <div
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`} className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`}

View File

@ -2,7 +2,8 @@ import React from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
// components
import { PageHead } from "components/core";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// ui // ui
@ -14,6 +15,7 @@ import type { NextPage } from "next";
const PageNotFound: NextPage = () => ( const PageNotFound: NextPage = () => (
<DefaultLayout> <DefaultLayout>
<PageHead title="404 - Page Not Found" />
<div className="grid h-full place-items-center p-4"> <div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center"> <div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80"> <div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">

View File

@ -1,13 +1,28 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react";
// components // components
import { PageHead } from "components/core";
import { WorkspaceActiveCycleHeader } from "components/headers"; import { WorkspaceActiveCycleHeader } from "components/headers";
import { WorkspaceActiveCyclesUpgrade } from "components/workspace"; import { WorkspaceActiveCyclesUpgrade } from "components/workspace";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useWorkspace } from "hooks/store";
const WorkspaceActiveCyclesPage: NextPageWithLayout = () => <WorkspaceActiveCyclesUpgrade />; const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => {
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Active Cycles` : undefined;
return (
<>
<PageHead title={pageTitle} />
<WorkspaceActiveCyclesUpgrade />
</>
);
});
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) { WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>; return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;

View File

@ -1,22 +1,23 @@
import React, { Fragment, ReactElement } from "react"; import React, { Fragment, ReactElement } from "react";
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";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
import { WorkspaceAnalyticsHeader } from "components/headers"; import { WorkspaceAnalyticsHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants // constants
import { ANALYTICS_TABS } from "constants/analytics"; import { ANALYTICS_TABS } from "constants/analytics";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
// type // type
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { useRouter } from "next/router";
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
const AnalyticsPage: NextPageWithLayout = observer(() => { const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
@ -33,13 +34,16 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
currentUser, currentUser,
} = useUser(); } = useUser();
const { workspaceProjectIds } = useProject(); const { workspaceProjectIds } = useProject();
const { currentWorkspace } = useWorkspace();
// derived values
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
return ( return (
<> <>
<PageHead title={pageTitle} />
{workspaceProjectIds && workspaceProjectIds.length > 0 ? ( {workspaceProjectIds && workspaceProjectIds.length > 0 ? (
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100"> <div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}> <Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>

View File

@ -1,13 +1,28 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { WorkspaceDashboardView } from "components/page-views"; import { WorkspaceDashboardView } from "components/page-views";
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard"; import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useWorkspace } from "hooks/store";
const WorkspacePage: NextPageWithLayout = () => <WorkspaceDashboardView />; const WorkspacePage: NextPageWithLayout = observer(() => {
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Dashboard` : undefined;
return (
<>
<PageHead title={pageTitle} />
<WorkspaceDashboardView />
</>
);
});
WorkspacePage.getLayout = function getLayout(page: ReactElement) { WorkspacePage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>; return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>;

View File

@ -4,11 +4,17 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components // components
import { UserProfileHeader } from "components/headers"; import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { ProfileIssuesPage } from "components/profile/profile-issues"; import { ProfileIssuesPage } from "components/profile/profile-issues";
const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="assigned" />; const ProfileAssignedIssuesPage: NextPageWithLayout = () => (
<>
<PageHead title="Profile - Assigned" />
<ProfileIssuesPage type="assigned" />
</>
);
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components // components
import { UserProfileHeader } from "components/headers"; import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { ProfileIssuesPage } from "components/profile/profile-issues"; import { ProfileIssuesPage } from "components/profile/profile-issues";
const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="created" />; const ProfileCreatedIssuesPage: NextPageWithLayout = () => (
<>
<PageHead title="Profile - Created" />
<ProfileIssuesPage type="created" />
</>
);
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -8,6 +8,7 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components // components
import { UserProfileHeader } from "components/headers"; import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
import { import {
ProfileActivity, ProfileActivity,
ProfilePriorityDistribution, ProfilePriorityDistribution,
@ -42,6 +43,8 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
}); });
return ( return (
<>
<PageHead title="Profile - Summary" />
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9"> <div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
<ProfileStats userProfile={userProfile} /> <ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} /> <ProfileWorkload stateDistribution={stateDistribution} />
@ -51,12 +54,13 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
</div> </div>
<ProfileActivity /> <ProfileActivity />
</div> </div>
</>
); );
}; };
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) { ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader type='Summary' />}> <AppLayout header={<UserProfileHeader type="Summary" />}>
<ProfileAuthWrapper>{page}</ProfileAuthWrapper> <ProfileAuthWrapper>{page}</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );

View File

@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components // components
import { UserProfileHeader } from "components/headers"; import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { ProfileIssuesPage } from "components/profile/profile-issues"; import { ProfileIssuesPage } from "components/profile/profile-issues";
const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="subscribed" />; const ProfileSubscribedIssuesPage: NextPageWithLayout = () => (
<>
<PageHead title="Profile - Subscribed" />
<ProfileIssuesPage type="subscribed" />
</>
);
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,5 +1,6 @@
import { useState, ReactElement } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -9,6 +10,7 @@ import { AppLayout } from "layouts/app-layout";
// components // components
import { IssueDetailRoot } from "components/issues"; import { IssueDetailRoot } from "components/issues";
import { ProjectArchivedIssueDetailsHeader } from "components/headers"; import { ProjectArchivedIssueDetailsHeader } from "components/headers";
import { PageHead } from "components/core";
// ui // ui
import { ArchiveIcon, Loader } from "@plane/ui"; import { ArchiveIcon, Loader } from "@plane/ui";
// icons // icons
@ -18,7 +20,7 @@ import { NextPageWithLayout } from "lib/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
const ArchivedIssueDetailsPage: NextPageWithLayout = () => { const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, archivedIssueId } = router.query; const { workspaceSlug, projectId, archivedIssueId } = router.query;
@ -45,6 +47,9 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
); );
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined; const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
if (!issue) return <></>; if (!issue) return <></>;
const handleUnArchive = async () => { const handleUnArchive = async () => {
@ -79,6 +84,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
return ( return (
<> <>
<PageHead title={pageTitle} />
{issueLoader ? ( {issueLoader ? (
<Loader className="flex h-full gap-5 p-5"> <Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2"> <div className="basis-2/3 space-y-2">
@ -126,7 +132,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
)} )}
</> </>
); );
}; });
ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) { ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,22 +1,34 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// contexts // contexts
import { ArchivedIssueLayoutRoot } from "components/issues"; import { ArchivedIssueLayoutRoot } from "components/issues";
// ui // ui
import { ArchiveIcon } from "@plane/ui"; import { ArchiveIcon } from "@plane/ui";
// components
import { ProjectArchivedIssuesHeader } from "components/headers"; import { ProjectArchivedIssuesHeader } from "components/headers";
import { PageHead } from "components/core";
// icons // icons
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
const ProjectArchivedIssuesPage: NextPageWithLayout = () => { const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
const { getProjectById } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && `${project?.name} - Archived Issues`;
return ( return (
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm"> <div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
<button <button
@ -31,8 +43,9 @@ const ProjectArchivedIssuesPage: NextPageWithLayout = () => {
</div> </div>
<ArchivedIssueLayoutRoot /> <ArchivedIssueLayoutRoot />
</div> </div>
</>
); );
}; });
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,12 +1,14 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react";
// hooks // hooks
import { useCycle } from "hooks/store"; import { useCycle, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { CycleIssuesHeader } from "components/headers"; import { CycleIssuesHeader } from "components/headers";
import { CycleDetailsSidebar } from "components/cycles"; import { CycleDetailsSidebar } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts"; import { CycleLayoutRoot } from "components/issues/issue-layouts";
@ -17,27 +19,36 @@ import emptyCycle from "public/empty-state/cycle.svg";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const CycleDetailPage: NextPageWithLayout = () => { const CycleDetailPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query;
// store hooks // store hooks
const { fetchCycleDetails } = useCycle(); const { fetchCycleDetails, getCycleById } = useCycle();
const { getProjectById } = useProject();
// hooks
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; // fetching cycle details
const { error } = useSWR( const { error } = useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
workspaceSlug && projectId && cycleId workspaceSlug && projectId && cycleId
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
: null : null
); );
// derived values
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined;
/**
* Toggles the sidebar
*/
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`); const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
return ( return (
<> <>
<PageHead title={pageTitle} />
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyCycle} image={emptyCycle}
@ -70,7 +81,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
)} )}
</> </>
); );
}; });
CycleDetailPage.getLayout = function getLayout(page: ReactElement) { CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -4,11 +4,12 @@ import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// hooks // hooks
import { useEventTracker, useCycle, useUser } from "hooks/store"; import { useEventTracker, useCycle, useUser, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { CyclesHeader } from "components/headers"; import { CyclesHeader } from "components/headers";
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
@ -34,12 +35,20 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
currentUser, currentUser,
} = useUser(); } = useUser();
const { currentProjectCycleIds, loader } = useCycle(); const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById } = useProject();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
// local storage // local storage
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active"); const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
// derived values
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
const totalCycles = currentProjectCycleIds?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
const handleCurrentLayout = useCallback( const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => { (_layout: TCycleLayout) => {
@ -56,13 +65,6 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
[handleCurrentLayout, setCycleTab] [handleCurrentLayout, setCycleTab]
); );
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
const totalCycles = currentProjectCycleIds?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!workspaceSlug || !projectId) return null; if (!workspaceSlug || !projectId) return null;
if (loader) if (loader)
@ -75,6 +77,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<>
<PageHead title={pageTitle} />
<div className="w-full h-full"> <div className="w-full h-full">
<CycleCreateUpdateModal <CycleCreateUpdateModal
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
@ -212,6 +216,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab.Group> </Tab.Group>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -5,15 +5,26 @@ import { X, PenSquare } from "lucide-react";
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root";
import { PageHead } from "components/core";
import { ProjectDraftIssueHeader } from "components/headers"; import { ProjectDraftIssueHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
import { observer } from "mobx-react";
const ProjectDraftIssuesPage: NextPageWithLayout = () => { const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store
const { getProjectById } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Draft Issues` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm"> <div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
<button <button
@ -23,13 +34,14 @@ const ProjectDraftIssuesPage: NextPageWithLayout = () => {
> >
<PenSquare className="h-4 w-4" /> <PenSquare className="h-4 w-4" />
<span>Draft Issues</span> <span>Draft Issues</span>
<X className="h-3 w-3" />
</button> </button>
<X className="h-3 w-3" />
</div> </div>
<DraftIssueLayoutRoot /> <DraftIssueLayoutRoot />
</div> </div>
</>
); );
}; });
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) { ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -7,9 +7,9 @@ import { useProject, useInboxIssues } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { ProjectInboxHeader } from "components/headers"; import { ProjectInboxHeader } from "components/headers";
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
@ -22,7 +22,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
filters: { fetchInboxFilters }, filters: { fetchInboxFilters },
issues: { fetchInboxIssues }, issues: { fetchInboxIssues },
} = useInboxIssues(); } = useInboxIssues();
// fetching the Inbox filters and issues
useSWR( useSWR(
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` ? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
@ -34,9 +34,14 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
} }
} }
); );
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>; if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
return ( return (
<>
<PageHead title={pageTitle} />
<div className="relative flex h-full overflow-hidden"> <div className="relative flex h-full overflow-hidden">
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300"> <div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
<InboxSidebarRoot <InboxSidebarRoot
@ -54,6 +59,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
/> />
</div> </div>
</div> </div>
</>
); );
}); });

View File

@ -5,14 +5,15 @@ import { observer } from "mobx-react-lite";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { ProjectIssueDetailsHeader } from "components/headers"; import { ProjectIssueDetailsHeader } from "components/headers";
import { IssueDetailRoot } from "components/issues"; import { IssueDetailRoot } from "components/issues";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// fetch-keys // store hooks
import { useApplication, useIssueDetail } from "hooks/store"; import { useApplication, useIssueDetail, useProject } from "hooks/store";
const IssueDetailsPage: NextPageWithLayout = observer(() => { const IssueDetailsPage: NextPageWithLayout = observer(() => {
// router // router
@ -23,17 +24,20 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
fetchIssue, fetchIssue,
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { getProjectById } = useProject();
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
// fetching issue details
const { isLoading } = useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
workspaceSlug && projectId && issueId workspaceSlug && projectId && issueId
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()) ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
: null : null
); );
// derived values
const issue = getIssueById(issueId?.toString() || "") || undefined; const issue = getIssueById(issueId?.toString() || "") || undefined;
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
const issueLoader = !issue || isLoading ? true : false; const issueLoader = !issue || isLoading ? true : false;
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
useEffect(() => { useEffect(() => {
const handleToggleIssueDetailSidebar = () => { const handleToggleIssueDetailSidebar = () => {
@ -52,6 +56,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title={pageTitle} />
{issueLoader ? ( {issueLoader ? (
<Loader className="flex h-full gap-5 p-5"> <Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2"> <div className="basis-2/3 space-y-2">

View File

@ -1,4 +1,7 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// components // components
import { ProjectLayoutRoot } from "components/issues"; import { ProjectLayoutRoot } from "components/issues";
import { ProjectIssuesHeader } from "components/headers"; import { ProjectIssuesHeader } from "components/headers";
@ -6,12 +9,36 @@ import { ProjectIssuesHeader } from "components/headers";
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// hooks
import { useProject } from "hooks/store";
import { PageHead } from "components/core";
const ProjectIssuesPage: NextPageWithLayout = () => ( const ProjectIssuesPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { projectId } = router.query;
// store
const { getProjectById } = useProject();
if (!projectId) {
return <></>;
}
// derived values
const project = getProjectById(projectId.toString());
const pageTitle = project?.name ? `${project?.name} - Issues` : undefined;
return (
<>
<PageHead title={pageTitle} />
<Head>
<title>{project?.name} - Issues</title>
</Head>
<div className="h-full w-full"> <div className="h-full w-full">
<ProjectLayoutRoot /> <ProjectLayoutRoot />
</div> </div>
); </>
);
});
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) { ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,8 +1,9 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react";
// hooks // hooks
import { useModule } from "hooks/store"; import { useModule, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
@ -10,37 +11,44 @@ import { AppLayout } from "layouts/app-layout";
import { ModuleDetailsSidebar } from "components/modules"; import { ModuleDetailsSidebar } from "components/modules";
import { ModuleLayoutRoot } from "components/issues"; import { ModuleLayoutRoot } from "components/issues";
import { ModuleIssuesHeader } from "components/headers"; import { ModuleIssuesHeader } from "components/headers";
// ui import { PageHead } from "components/core";
import { EmptyState } from "components/common"; 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 { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const ModuleIssuesPage: NextPageWithLayout = () => { const ModuleIssuesPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
// store hooks // store hooks
const { fetchModuleDetails } = useModule(); const { fetchModuleDetails, getModuleById } = useModule();
const { getProjectById } = useProject();
// local storage // 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;
// fetching module details
const { error } = useSWR( const { error } = useSWR(
workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null, workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null,
workspaceSlug && projectId && moduleId workspaceSlug && projectId && moduleId
? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()) ? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
: null : null
); );
// derived values
const projectModule = moduleId ? getModuleById(moduleId.toString()) : undefined;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && projectModule?.name ? `${project?.name} - ${projectModule?.name}` : undefined;
const toggleSidebar = () => { const toggleSidebar = () => {
setValue(`${!isSidebarCollapsed}`); setValue(`${!isSidebarCollapsed}`);
}; };
if (!workspaceSlug || !projectId || !moduleId) return <></>; if (!workspaceSlug || !projectId || !moduleId) return <></>;
return ( return (
<> <>
<PageHead title={pageTitle} />
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyModule} image={emptyModule}
@ -71,7 +79,7 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
)} )}
</> </>
); );
}; });
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) { ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,13 +1,33 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { ModulesListView } from "components/modules"; import { ModulesListView } from "components/modules";
import { ModulesListHeader } from "components/headers"; import { ModulesListHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
import { observer } from "mobx-react";
const ProjectModulesPage: NextPageWithLayout = () => <ModulesListView />; const ProjectModulesPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { projectId } = router.query;
// store
const { getProjectById } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
return (
<>
<PageHead title={pageTitle} />
<ModulesListView />
</>
);
});
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) { ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -14,7 +14,7 @@ import { FileService } from "services/file.service";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { GptAssistantPopover } from "components/core"; import { GptAssistantPopover, PageHead } from "components/core";
import { PageDetailsHeader } from "components/headers/page-details"; import { PageDetailsHeader } from "components/headers/page-details";
// ui // ui
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
@ -256,6 +256,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return pageIdMobx ? ( return pageIdMobx ? (
<>
<PageHead title={pageTitle} />
<div className="flex h-full flex-col justify-between"> <div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
{isPageReadOnly ? ( {isPageReadOnly ? (
@ -363,6 +365,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
<IssuePeekOverview /> <IssuePeekOverview />
</div> </div>
</div> </div>
</>
) : ( ) : (
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">
<Spinner /> <Spinner />

View File

@ -6,7 +6,7 @@ import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useEventTracker, useUser } from "hooks/store"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useSize from "hooks/use-window-size"; import useSize from "hooks/use-window-size";
@ -24,6 +24,7 @@ import { PAGE_TABS_LIST } from "constants/page";
import { useProjectPages } from "hooks/store/use-project-page"; import { useProjectPages } from "hooks/store/use-project-page";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { PageHead } from "components/core";
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), { const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false, ssr: false,
@ -63,7 +64,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
commandPalette: { toggleCreatePageModal }, commandPalette: { toggleCreatePageModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { getProjectById } = useProject();
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } = const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
useProjectPages(); useProjectPages();
// hooks // hooks
@ -101,10 +102,12 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
} }
}; };
// derived values
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
const MobileTabList = () => ( const MobileTabList = () => (
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4"> <Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
@ -129,6 +132,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title={pageTitle} />
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? ( {projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
<> <>
{workspaceSlug && projectId && ( {workspaceSlug && projectId && (

View File

@ -10,6 +10,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
@ -41,9 +42,13 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
}); });
}; };
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}> <section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Automations</h3> <h3 className="text-xl font-medium">Automations</h3>
@ -51,6 +56,7 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
<AutoArchiveAutomation handleChange={handleChange} /> <AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} /> <AutoCloseAutomation handleChange={handleChange} />
</section> </section>
</>
); );
}); });

View File

@ -1,11 +1,12 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser, useProject } from "hooks/store";
// 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";
// components // components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { EstimatesList } from "components/estimates"; import { EstimatesList } from "components/estimates";
// types // types
@ -17,13 +18,18 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject();
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}> <div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
<EstimatesList /> <EstimatesList />
</div> </div>
</>
); );
}); });

View File

@ -1,41 +1,48 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react";
// hooks // hooks
import { useUser } from "hooks/store"; import { useProject, useUser } from "hooks/store";
// 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";
// components // components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { ProjectFeaturesList } from "components/project"; import { ProjectFeaturesList } from "components/project";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const FeaturesSettingsPage: NextPageWithLayout = () => { const FeaturesSettingsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { const {
membership: { fetchUserProjectInfo }, membership: { fetchUserProjectInfo },
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject();
// fetch the project details
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 ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
); );
// derived values
const isAdmin = memberDetails?.role === 20; const isAdmin = memberDetails?.role === 20;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}> <section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Features</h3> <h3 className="text-xl font-medium">Features</h3>
</div> </div>
<ProjectFeaturesList /> <ProjectFeaturesList />
</section> </section>
</>
); );
}; });
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) { FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -8,6 +8,7 @@ import { useProject } from "hooks/store";
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
// components // components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { import {
DeleteProjectModal, DeleteProjectModal,
@ -32,14 +33,15 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
); );
// derived values
const isAdmin = currentProjectDetails?.member_role === 20;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); // const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); // const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
const isAdmin = currentProjectDetails?.member_role === 20;
return ( return (
<> <>
<PageHead title={pageTitle} />
{currentProjectDetails && ( {currentProjectDetails && (
<DeleteProjectModal <DeleteProjectModal
project={currentProjectDetails} project={currentProjectDetails}

View File

@ -2,6 +2,7 @@ import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { observer } from "mobx-react";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser } from "hooks/store";
// layouts // layouts
@ -11,6 +12,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// components // components
import { PageHead } from "components/core";
import { IntegrationCard } from "components/project"; import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
@ -27,31 +29,33 @@ import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
const projectService = new ProjectService(); const projectService = new ProjectService();
const ProjectIntegrationsPage: NextPageWithLayout = () => { const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { currentUser } = useUser(); const { currentUser } = useUser();
// fetch project details
const { data: projectDetails } = useSWR<IProject>( const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
); );
// fetch Integrations list
const { data: workspaceIntegrations } = useSWR( const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null, workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
); );
// derived values
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
const isAdmin = projectDetails?.member_role === 20; const isAdmin = projectDetails?.member_role === 20;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}> <div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Integrations</h3> <h3 className="text-xl font-medium">Integrations</h3>
@ -82,8 +86,9 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
<IntegrationsSettingsLoader /> <IntegrationsSettingsLoader />
)} )}
</div> </div>
</>
); );
}; });
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) { ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,18 +1,30 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-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";
// components // components
import { PageHead } from "components/core";
import { ProjectSettingsLabelList } from "components/labels"; import { ProjectSettingsLabelList } from "components/labels";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
const LabelsSettingsPage: NextPageWithLayout = () => ( const LabelsSettingsPage: NextPageWithLayout = observer(() => {
const { currentProjectDetails } = useProject();
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
return (
<>
<PageHead title={pageTitle} />
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9"> <div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
<ProjectSettingsLabelList /> <ProjectSettingsLabelList />
</div> </div>
); </>
);
});
LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) { LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,19 +1,33 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-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";
// components // components
import { PageHead } from "components/core";
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 { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
const MembersSettingsPage: NextPageWithLayout = () => ( const MembersSettingsPage: NextPageWithLayout = observer(() => {
// store
const { currentProjectDetails } = useProject();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto py-8 pr-9`}> <section className={`w-full overflow-y-auto py-8 pr-9`}>
<ProjectSettingsMemberDefaults /> <ProjectSettingsMemberDefaults />
<ProjectMemberList /> <ProjectMemberList />
</section> </section>
); </>
);
});
MembersSettingsPage.getLayout = function getLayout(page: ReactElement) { MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,13 +1,15 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react";
// hooks // hooks
import { useProjectView } from "hooks/store"; import { useProject, useProjectView } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { ProjectViewLayoutRoot } from "components/issues"; import { ProjectViewLayoutRoot } from "components/issues";
import { ProjectViewIssuesHeader } from "components/headers"; import { ProjectViewIssuesHeader } from "components/headers";
import { PageHead } from "components/core";
// ui // ui
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
// assets // assets
@ -15,12 +17,17 @@ import emptyView from "public/empty-state/view.svg";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const ProjectViewIssuesPage: NextPageWithLayout = () => { const ProjectViewIssuesPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query;
// store hooks // store hooks
const { fetchViewDetails } = useProjectView(); const { fetchViewDetails, getViewById } = useProjectView();
const { getProjectById } = useProject();
// derived values
const projectView = viewId ? getViewById(viewId.toString()) : undefined;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && projectView?.name ? `${project?.name} - ${projectView?.name}` : undefined;
const { error } = useSWR( const { error } = useSWR(
workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null, workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null,
@ -42,11 +49,14 @@ const ProjectViewIssuesPage: NextPageWithLayout = () => {
}} }}
/> />
) : ( ) : (
<>
<PageHead title={pageTitle} />
<ProjectViewLayoutRoot /> <ProjectViewLayoutRoot />
</>
)} )}
</> </>
); );
}; });
ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) { ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,13 +1,34 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// components // components
import { ProjectViewsHeader } from "components/headers"; import { ProjectViewsHeader } from "components/headers";
import { ProjectViewsList } from "components/views"; import { ProjectViewsList } from "components/views";
import { PageHead } from "components/core";
// hooks
import { useProject } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const ProjectViewsPage: NextPageWithLayout = () => <ProjectViewsList />; const ProjectViewsPage: NextPageWithLayout = observer(() => {
// router
const router = useRouter();
const { projectId } = router.query;
// store
const { getProjectById } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
return (
<>
<PageHead title={pageTitle} />
<ProjectViewsList />
</>
);
});
ProjectViewsPage.getLayout = function getLayout(page: ReactElement) { ProjectViewsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -1,13 +1,28 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react";
// components // components
import { PageHead } from "components/core";
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 // type
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { useWorkspace } from "hooks/store";
const ProjectsPage: NextPageWithLayout = () => <ProjectCardList />; const ProjectsPage: NextPageWithLayout = observer(() => {
// store
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined;
return (
<>
<PageHead title={pageTitle} />
<ProjectCardList />
</>
);
});
ProjectsPage.getLayout = function getLayout(page: ReactElement) { ProjectsPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>; return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// store hooks // store hooks
import { useUser } from "hooks/store"; import { useUser, useWorkspace } from "hooks/store";
// 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";
@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types";
import { API_TOKENS_LIST } from "constants/fetch-keys"; import { API_TOKENS_LIST } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { PageHead } from "components/core";
const apiTokenService = new APITokenService(); const apiTokenService = new APITokenService();
@ -39,6 +40,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
currentUser, currentUser,
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
@ -49,12 +51,16 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
if (!tokens) { if (!tokens) {
@ -63,6 +69,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title={pageTitle} />
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} /> <CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
<section className="h-full w-full overflow-y-auto py-8 pr-9"> <section className="h-full w-full overflow-y-auto py-8 pr-9">
{tokens.length > 0 ? ( {tokens.length > 0 ? (

View File

@ -1,11 +1,12 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser, useWorkspace } from "hooks/store";
// 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";
// component // component
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { PageHead } from "components/core";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
@ -18,17 +19,24 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
return ( return (
<>
<PageHead title={pageTitle} />
<section className="w-full overflow-y-auto py-8 pr-9"> <section className="w-full overflow-y-auto py-8 pr-9">
<div> <div>
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
@ -45,6 +53,7 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => {
</div> </div>
</div> </div>
</section> </section>
</>
); );
}); });

View File

@ -1,12 +1,13 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser, useWorkspace } from "hooks/store";
// 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";
// components // components
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import ExportGuide from "components/exporter/guide"; import ExportGuide from "components/exporter/guide";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
@ -17,24 +18,33 @@ const ExportsPage: NextPageWithLayout = observer(() => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const hasPageAccess = const hasPageAccess =
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
if (!hasPageAccess) if (!hasPageAccess)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
return ( return (
<>
<PageHead title={pageTitle} />
<div className="w-full overflow-y-auto py-8 pr-9"> <div className="w-full overflow-y-auto py-8 pr-9">
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Exports</h3> <h3 className="text-xl font-medium">Exports</h3>
</div> </div>
<ExportGuide /> <ExportGuide />
</div> </div>
</>
); );
}); });

View File

@ -1,12 +1,13 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser, useWorkspace } from "hooks/store";
// layouts // layouts
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
@ -17,23 +18,32 @@ const ImportsPage: NextPageWithLayout = observer(() => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
return ( return (
<>
<PageHead title={pageTitle} />
<section className="w-full overflow-y-auto py-8 pr-9"> <section className="w-full overflow-y-auto py-8 pr-9">
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Imports</h3> <h3 className="text-xl font-medium">Imports</h3>
</div> </div>
<IntegrationGuide /> <IntegrationGuide />
</section> </section>
</>
); );
}); });

View File

@ -1,14 +1,30 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-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";
// hooks
import { useWorkspace } from "hooks/store";
// components // components
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { WorkspaceDetails } from "components/workspace"; import { WorkspaceDetails } from "components/workspace";
import { PageHead } from "components/core";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
const WorkspaceSettingsPage: NextPageWithLayout = () => <WorkspaceDetails />; const WorkspaceSettingsPage: NextPageWithLayout = observer(() => {
// store hooks
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - General Settings` : undefined;
return (
<>
<PageHead title={pageTitle} />
<WorkspaceDetails />
</>
);
});
WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) { WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser, useWorkspace } from "hooks/store";
// services // services
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
// layouts // layouts
@ -12,6 +12,7 @@ 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 { PageHead } from "components/core";
// ui // ui
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui"; import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
// types // types
@ -31,14 +32,20 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () => const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
@ -46,16 +53,21 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<>
<PageHead title={pageTitle} />
<section className="w-full overflow-y-auto py-8 pr-9"> <section className="w-full overflow-y-auto py-8 pr-9">
<IntegrationAndImportExportBanner bannerName="Integrations" /> <IntegrationAndImportExportBanner bannerName="Integrations" />
<div> <div>
{appIntegrations ? ( {appIntegrations ? (
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />) appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} />
))
) : ( ) : (
<IntegrationsSettingsLoader /> <IntegrationsSettingsLoader />
)} )}
</div> </div>
</section> </section>
</>
); );
}); });

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// hooks // hooks
import { useEventTracker, useMember, useUser } from "hooks/store"; import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
@ -11,6 +11,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components // components
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
import { PageHead } from "components/core";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
@ -37,6 +38,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
const { const {
workspace: { inviteMembersToWorkspace }, workspace: { inviteMembersToWorkspace },
} = useMember(); } = useMember();
const { currentWorkspace } = useWorkspace();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -83,11 +85,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
}); });
}; };
// derived values
const hasAddMemberPermission = const hasAddMemberPermission =
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
return ( return (
<> <>
<PageHead title={pageTitle} />
<SendWorkspaceInvitationModal <SendWorkspaceInvitationModal
isOpen={inviteModal} isOpen={inviteModal}
onClose={() => setInviteModal(false)} onClose={() => setInviteModal(false)}

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useUser, useWebhook } from "hooks/store"; import { useUser, useWebhook, useWorkspace } from "hooks/store";
// 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";
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
// components // components
import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceSettingHeader } from "components/headers";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
import { PageHead } from "components/core";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types // types
@ -29,6 +30,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -38,6 +40,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
// }, [clearSecretKey, isCreated]); // }, [clearSecretKey, isCreated]);
const isAdmin = currentWorkspaceRole === 20; const isAdmin = currentWorkspaceRole === 20;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
useSWR( useSWR(
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null, workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
@ -76,9 +79,12 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
if (!currentWebhook) if (!currentWebhook)
@ -90,6 +96,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title={pageTitle} />
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} /> <DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9"> <div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} /> <WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />

View File

@ -19,6 +19,7 @@ import { WebhookSettingsLoader } from "components/ui";
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { PageHead } from "components/core";
const WebhooksListPage: NextPageWithLayout = observer(() => { const WebhooksListPage: NextPageWithLayout = observer(() => {
// states // states
@ -47,6 +48,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
// clear secret key when modal is closed. // clear secret key when modal is closed.
useEffect(() => { useEffect(() => {
@ -55,14 +57,19 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
if (!isAdmin) if (!isAdmin)
return ( return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4"> <div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p> <p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div> </div>
</>
); );
if (!webhooks) return <WebhookSettingsLoader />; if (!webhooks) return <WebhookSettingsLoader />;
return ( return (
<>
<PageHead title={pageTitle} />
<div className="h-full w-full overflow-hidden py-8 pr-9"> <div className="h-full w-full overflow-hidden py-8 pr-9">
<CreateWebhookModal <CreateWebhookModal
createWebhook={createWebhook} createWebhook={createWebhook}
@ -102,6 +109,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
</div> </div>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -7,15 +7,26 @@ import { AllIssueLayoutRoot } from "components/issues";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { observer } from "mobx-react";
import { useWorkspace } from "hooks/store";
import { PageHead } from "components/core";
const GlobalViewIssuesPage: NextPageWithLayout = () => ( const GlobalViewIssuesPage: NextPageWithLayout = observer(() => {
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined;
return (
<>
<PageHead title={pageTitle} />
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300"> <div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader /> <GlobalViewsHeader />
<AllIssueLayoutRoot /> <AllIssueLayoutRoot />
</div> </div>
</div> </div>
); </>
);
});
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) { GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>; return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;

View File

@ -1,7 +1,9 @@
import React, { useState, ReactElement } from "react"; import React, { useState, ReactElement } from "react";
import { observer } from "mobx-react";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageHead } from "components/core";
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers"; import { GlobalIssuesHeader } from "components/headers";
// ui // ui
@ -12,11 +14,19 @@ import { Search } from "lucide-react";
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
// hooks
import { useWorkspace } from "hooks/store";
const WorkspaceViewsPage: NextPageWithLayout = () => { const WorkspaceViewsPage: NextPageWithLayout = observer(() => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
// store
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
return ( return (
<>
<PageHead title={pageTitle} />
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex h-full w-full flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3"> <div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
@ -35,8 +45,9 @@ const WorkspaceViewsPage: NextPageWithLayout = () => {
))} ))}
<GlobalViewsList searchQuery={query} /> <GlobalViewsList searchQuery={query} />
</div> </div>
</>
); );
}; });
WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) { WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>; return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;

View File

@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store";
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { LatestFeatureBlock } from "components/common"; import { LatestFeatureBlock } from "components/common";
import { PageHead } from "components/core";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// images // images
@ -85,6 +86,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
}; };
return ( return (
<>
<PageHead title="Forgot Password" />
<div className="h-full w-full bg-onboarding-gradient-100"> <div className="h-full w-full bg-onboarding-gradient-100">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 "> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
<div className="flex items-center gap-x-2 py-10"> <div className="flex items-center gap-x-2 py-10">
@ -138,6 +141,7 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store";
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { LatestFeatureBlock } from "components/common"; import { LatestFeatureBlock } from "components/common";
import { PageHead } from "components/core";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// images // images
@ -90,6 +91,8 @@ const ResetPasswordPage: NextPageWithLayout = () => {
}; };
return ( return (
<>
<PageHead title="Reset Password" />
<div className="h-full w-full bg-onboarding-gradient-100"> <div className="h-full w-full bg-onboarding-gradient-100">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 "> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
<div className="flex items-center gap-x-2 py-10"> <div className="flex items-center gap-x-2 py-10">
@ -174,6 +177,7 @@ const ResetPasswordPage: NextPageWithLayout = () => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -7,6 +7,7 @@ import { useApplication, useUser } from "hooks/store";
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { SignUpRoot } from "components/account"; import { SignUpRoot } from "components/account";
import { PageHead } from "components/core";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// assets // assets
@ -29,6 +30,8 @@ const SignUpPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<>
<PageHead title="Sign Up" />
<div className="h-full w-full bg-onboarding-gradient-100"> <div className="h-full w-full bg-onboarding-gradient-100">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28"> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="flex items-center gap-x-2 py-10"> <div className="flex items-center gap-x-2 py-10">
@ -43,6 +46,7 @@ const SignUpPage: NextPageWithLayout = observer(() => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}); });

View File

@ -11,6 +11,7 @@ import DefaultLayout from "layouts/default-layout";
import { UserAuthWrapper } from "layouts/auth-layout"; import { UserAuthWrapper } from "layouts/auth-layout";
// components // components
import { CreateWorkspaceForm } from "components/workspace"; import { CreateWorkspaceForm } from "components/workspace";
import { PageHead } from "components/core";
// images // images
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
@ -37,6 +38,8 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<>
<PageHead title="Create Workspace" />
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0"> <div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-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="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" /> <div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
@ -69,6 +72,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}); });

View File

@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
import { Lightbulb } from "lucide-react"; import { Lightbulb } from "lucide-react";
// components // components
import { InstanceAIForm } from "components/instance"; import { InstanceAIForm } from "components/instance";
import { PageHead } from "components/core";
const InstanceAdminAIPage: NextPageWithLayout = observer(() => { const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
// store // store
@ -23,6 +24,8 @@ const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<>
<PageHead title="God Mode - AI" />
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3"> <div className="mb-2 border-b border-custom-border-100 pb-3">
<div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div> <div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
@ -54,6 +57,7 @@ const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -14,6 +14,7 @@ import useToast from "hooks/use-toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
// components // components
import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance";
import { PageHead } from "components/core";
const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
// store // store
@ -64,6 +65,8 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<>
<PageHead title="God Mode - SSO and OAuth" />
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3"> <div className="mb-2 border-b border-custom-border-100 pb-3">
<div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div> <div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div>
@ -176,6 +179,7 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { InstanceEmailForm } from "components/instance"; import { InstanceEmailForm } from "components/instance";
import { PageHead } from "components/core";
const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
// store // store
@ -21,6 +22,8 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<>
<PageHead title="God Mode - Email" />
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3"> <div className="mb-2 border-b border-custom-border-100 pb-3">
<div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div> <div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
@ -44,6 +47,7 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { InstanceImageConfigForm } from "components/instance"; import { InstanceImageConfigForm } from "components/instance";
import { PageHead } from "components/core";
const InstanceAdminImagePage: NextPageWithLayout = observer(() => { const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
// store // store
@ -21,6 +22,8 @@ const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<>
<PageHead title="God Mode - Images" />
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3"> <div className="mb-2 border-b border-custom-border-100 pb-3">
<div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div> <div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div>
@ -40,6 +43,7 @@ const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { InstanceGeneralForm } from "components/instance"; import { InstanceGeneralForm } from "components/instance";
import { PageHead } from "components/core";
const InstanceAdminPage: NextPageWithLayout = observer(() => { const InstanceAdminPage: NextPageWithLayout = observer(() => {
// store hooks // store hooks
@ -22,6 +23,8 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins()); useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
return ( return (
<>
<PageHead title="God Mode - General Settings" />
<div className="flex h-full w-full flex-col gap-8"> <div className="flex h-full w-full flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3"> <div className="mb-2 border-b border-custom-border-100 pb-3">
<div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div> <div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div>
@ -42,6 +45,7 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</div> </div>
</>
); );
}); });

View File

@ -32,7 +32,7 @@ import { ROLE } from "constants/workspace";
import { MEMBER_ACCEPTED } from "constants/event-tracker"; import { MEMBER_ACCEPTED } from "constants/event-tracker";
// components // components
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
import { PageHead } from "components/core";
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const userService = new UserService(); const userService = new UserService();
@ -126,6 +126,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<>
<PageHead title="Invitations" />
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0"> <div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-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="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" /> <div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
@ -228,6 +230,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
) )
) : null} ) : null}
</div> </div>
</>
); );
}); });

View File

@ -17,6 +17,7 @@ import DefaultLayout from "layouts/default-layout";
import { UserAuthWrapper } from "layouts/auth-layout"; import { UserAuthWrapper } from "layouts/auth-layout";
// components // components
import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding";
import { PageHead } from "components/core";
// ui // ui
import { Avatar, Spinner } from "@plane/ui"; import { Avatar, Spinner } from "@plane/ui";
// images // images
@ -142,6 +143,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title="Onboarding" />
<SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} /> <SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} />
{user && step !== null ? ( {user && step !== null ? (
<div className={`fixed flex h-full w-full flex-col bg-onboarding-gradient-100`}> <div className={`fixed flex h-full w-full flex-col bg-onboarding-gradient-100`}>

View File

@ -9,7 +9,7 @@ import { UserService } from "services/user.service";
// layouts // layouts
import { ProfileSettingsLayout } from "layouts/settings-layout"; import { ProfileSettingsLayout } from "layouts/settings-layout";
// components // components
import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core";
import { RichReadOnlyEditor } from "@plane/rich-text-editor"; import { RichReadOnlyEditor } from "@plane/rich-text-editor";
// icons // icons
import { History, MessageSquare } from "lucide-react"; import { History, MessageSquare } from "lucide-react";
@ -32,6 +32,8 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
return ( return (
<>
<PageHead title="Profile - Activity" />
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5"> <section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5"> <div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} /> <SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
@ -185,6 +187,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
<ActivitySettingsLoader /> <ActivitySettingsLoader />
)} )}
</section> </section>
</>
); );
}); });

View File

@ -6,6 +6,8 @@ import { Controller, useForm } from "react-hook-form";
import { useApplication, useUser } from "hooks/store"; import { useApplication, useUser } from "hooks/store";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// components
import { PageHead } from "components/core";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// layout // layout
@ -88,6 +90,8 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<>
<PageHead title="Profile - Change Password" />
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4"> <div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} /> <SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
@ -164,7 +168,9 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
/> />
)} )}
/> />
{errors.confirm_password && <span className="text-xs text-red-500">{errors.confirm_password.message}</span>} {errors.confirm_password && (
<span className="text-xs text-red-500">{errors.confirm_password.message}</span>
)}
</div> </div>
</div> </div>
@ -175,6 +181,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
</div> </div>
</form> </form>
</div> </div>
</>
); );
}); });

View File

@ -11,7 +11,7 @@ import useToast from "hooks/use-toast";
// layouts // layouts
import { ProfileSettingsLayout } from "layouts/settings-layout"; import { ProfileSettingsLayout } from "layouts/settings-layout";
// components // components
import { ImagePickerPopover, UserImageUploadModal } from "components/core"; import { ImagePickerPopover, UserImageUploadModal, PageHead } from "components/core";
import { DeactivateAccountModal } from "components/account"; import { DeactivateAccountModal } from "components/account";
// ui // ui
import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui"; import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui";
@ -57,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
// store hooks // store hooks
const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser();
// custom hooks // custom hooks
const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader }); const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
useEffect(() => { useEffect(() => {
@ -138,6 +138,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title="Profile - General Settings" />
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4"> <div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} /> <SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
@ -296,7 +297,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
ref={ref} ref={ref}
hasError={Boolean(errors.email)} hasError={Boolean(errors.email)}
placeholder="Enter your email" placeholder="Enter your email"
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : "" className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${
errors.email ? "border-red-500" : ""
}`} }`}
disabled disabled
/> />
@ -385,7 +387,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomSearchSelect <CustomSearchSelect
value={value} value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"} label={
value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"
}
options={timeZoneOptions} options={timeZoneOptions}
onChange={onChange} onChange={onChange}
optionsClassName="w-full" optionsClassName="w-full"
@ -409,7 +413,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<Disclosure as="div" className="border-t border-custom-border-100 px-8"> <Disclosure as="div" className="border-t border-custom-border-100 px-8">
{({ open }) => ( {({ open }) => (
<> <>
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4"> <Disclosure.Button
as="button"
type="button"
className="flex w-full items-center justify-between py-4"
>
<span className="text-lg tracking-tight">Deactivate account</span> <span className="text-lg tracking-tight">Deactivate account</span>
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
</Disclosure.Button> </Disclosure.Button>
@ -426,8 +434,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight"> <span className="text-sm tracking-tight">
The danger zone of the profile page is a critical area that requires careful consideration and The danger zone of the profile page is a critical area that requires careful consideration and
attention. When deactivating an account, all of the data and resources within that account will be attention. When deactivating an account, all of the data and resources within that account
permanently removed and cannot be recovered. will be permanently removed and cannot be recovered.
</span> </span>
<div> <div>
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}> <Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>

View File

@ -6,6 +6,7 @@ import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile
import { EmailSettingsLoader } from "components/ui"; import { EmailSettingsLoader } from "components/ui";
// components // components
import { EmailNotificationForm } from "components/profile/preferences"; import { EmailNotificationForm } from "components/profile/preferences";
import { PageHead } from "components/core";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// type // type
@ -25,9 +26,12 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => {
} }
return ( return (
<>
<PageHead title="Profile - Email Preference" />
<div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8"> <div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
<EmailNotificationForm data={data} /> <EmailNotificationForm data={data} />
</div> </div>
</>
); );
}; };

View File

@ -7,7 +7,7 @@ import useToast from "hooks/use-toast";
// layouts // layouts
import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
// components // components
import { CustomThemeSelector, ThemeSwitch } from "components/core"; import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core";
// ui // ui
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// constants // constants
@ -47,6 +47,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<PageHead title="Profile - Theme Prefrence" />
{currentUser ? ( {currentUser ? (
<div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8"> <div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
<div className="flex items-center border-b border-custom-border-100 pb-3.5"> <div className="flex items-center border-b border-custom-border-100 pb-3.5">