chore: adding page titles using project title. (#3692)

* chore: adding page titles

* chore: added title to remaining pages

* fix: added observer at required places

---------

Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
This commit is contained in:
sriram veeraghanta 2024-02-20 13:36:38 +05:30 committed by GitHub
parent 07a4cb1f7d
commit cf3b888465
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1684 additions and 1215 deletions

View file

@ -1,13 +1,28 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
// components
import { PageHead } from "components/core";
import { WorkspaceActiveCycleHeader } from "components/headers";
import { WorkspaceActiveCyclesUpgrade } from "components/workspace";
// layouts
import { AppLayout } from "layouts/app-layout";
// 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) {
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;

View file

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

View file

@ -1,13 +1,28 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { PageHead } from "components/core";
import { WorkspaceDashboardView } from "components/page-views";
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
// 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) {
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";
// components
import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types
import { NextPageWithLayout } from "lib/types";
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) {
return (

View file

@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components
import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types
import { NextPageWithLayout } from "lib/types";
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) {
return (

View file

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

View file

@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
// components
import { UserProfileHeader } from "components/headers";
import { PageHead } from "components/core";
// types
import { NextPageWithLayout } from "lib/types";
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) {
return (

View file

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

View file

@ -1,38 +1,51 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// layouts
import { AppLayout } from "layouts/app-layout";
// contexts
import { ArchivedIssueLayoutRoot } from "components/issues";
// ui
import { ArchiveIcon } from "@plane/ui";
// components
import { ProjectArchivedIssuesHeader } from "components/headers";
import { PageHead } from "components/core";
// icons
import { X } from "lucide-react";
// types
import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
const ProjectArchivedIssuesPage: NextPageWithLayout = () => {
const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
const router = useRouter();
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 (
<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">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<ArchiveIcon className="h-4 w-4" />
<span>Archived Issues</span>
<X className="h-3 w-3" />
</button>
<>
<PageHead title={pageTitle} />
<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">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<ArchiveIcon className="h-4 w-4" />
<span>Archived Issues</span>
<X className="h-3 w-3" />
</button>
</div>
<ArchivedIssueLayoutRoot />
</div>
<ArchivedIssueLayoutRoot />
</div>
</>
);
};
});
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

@ -4,11 +4,12 @@ import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
import { useTheme } from "next-themes";
// hooks
import { useEventTracker, useCycle, useUser } from "hooks/store";
import { useEventTracker, useCycle, useUser, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { PageHead } from "components/core";
import { CyclesHeader } from "components/headers";
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
@ -34,12 +35,20 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
currentUser,
} = useUser();
const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById } = useProject();
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
// local storage
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
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(
(_layout: TCycleLayout) => {
@ -56,13 +65,6 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
[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 (loader)
@ -75,143 +77,146 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
);
return (
<div className="w-full h-full">
<CycleCreateUpdateModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isOpen={createModal}
handleClose={() => setCreateModal(false)}
/>
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<EmptyState
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
image={EmptyStateImagePath}
comicBox={{
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
}}
primaryButton={{
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
onClick: () => {
setTrackElement("Cycle empty state");
setCreateModal(true);
},
}}
size="lg"
disabled={!isEditingAllowed}
/>
</div>
) : (
<Tab.Group
as="div"
className="flex h-full flex-col overflow-hidden"
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
>
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
{CYCLE_TAB_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`
}
>
{tab.name}
</Tab>
))}
</Tab.List>
<div className="hidden sm:block">
{cycleTab !== "active" && (
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key === "gantt" && cycleTab === "draft") return null;
return (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
</Tooltip>
);
})}
</div>
)}
</div>
<>
<PageHead title={pageTitle} />
<div className="w-full h-full">
<CycleCreateUpdateModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isOpen={createModal}
handleClose={() => setCreateModal(false)}
/>
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<EmptyState
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
image={EmptyStateImagePath}
comicBox={{
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
}}
primaryButton={{
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
onClick: () => {
setTrackElement("Cycle empty state");
setCreateModal(true);
},
}}
size="lg"
disabled={!isEditingAllowed}
/>
</div>
) : (
<Tab.Group
as="div"
className="flex h-full flex-col overflow-hidden"
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
>
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
{CYCLE_TAB_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`
}
>
{tab.name}
</Tab>
))}
</Tab.List>
<div className="hidden sm:block">
{cycleTab !== "active" && (
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key === "gantt" && cycleTab === "draft") return null;
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && (
<CyclesView
filter="all"
layout={cycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
return (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
</Tooltip>
);
})}
</div>
)}
</div>
</div>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</Tab.Panel>
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && (
<CyclesView
filter="all"
layout={cycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && (
<CyclesView
filter="upcoming"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter="completed"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && (
<CyclesView
filter="upcoming"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter="draft"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</div>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter="completed"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView
filter="draft"
layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
)}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</div>
</>
);
});

View file

@ -5,31 +5,43 @@ import { X, PenSquare } from "lucide-react";
import { AppLayout } from "layouts/app-layout";
// components
import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root";
import { PageHead } from "components/core";
import { ProjectDraftIssueHeader } from "components/headers";
// 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 { 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 (
<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">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-4 w-4" />
<span>Draft Issues</span>
<>
<PageHead title={pageTitle} />
<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">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-4 w-4" />
<span>Draft Issues</span>
</button>
<X className="h-3 w-3" />
</button>
</div>
<DraftIssueLayoutRoot />
</div>
<DraftIssueLayoutRoot />
</div>
</>
);
};
});
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

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

View file

@ -1,4 +1,7 @@
import { ReactElement } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// components
import { ProjectLayoutRoot } from "components/issues";
import { ProjectIssuesHeader } from "components/headers";
@ -6,12 +9,36 @@ import { ProjectIssuesHeader } from "components/headers";
import { NextPageWithLayout } from "lib/types";
// layouts
import { AppLayout } from "layouts/app-layout";
// hooks
import { useProject } from "hooks/store";
import { PageHead } from "components/core";
const ProjectIssuesPage: NextPageWithLayout = () => (
<div className="h-full w-full">
<ProjectLayoutRoot />
</div>
);
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">
<ProjectLayoutRoot />
</div>
</>
);
});
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

@ -1,13 +1,33 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { PageHead } from "components/core";
import { ModulesListView } from "components/modules";
import { ModulesListHeader } from "components/headers";
// 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) {
return (

View file

@ -14,7 +14,7 @@ import { FileService } from "services/file.service";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GptAssistantPopover } from "components/core";
import { GptAssistantPopover, PageHead } from "components/core";
import { PageDetailsHeader } from "components/headers/page-details";
// ui
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
@ -256,113 +256,116 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return pageIdMobx ? (
<div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden">
{isPageReadOnly ? (
<DocumentReadOnlyEditorWithRef
onActionCompleteHandler={actionCompleteAlert}
ref={editorRef}
value={pageDescription}
customClassName={"tracking-tight w-full px-0"}
borderOnFocus={false}
noBorder
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: archived_at ? new Date(archived_at) : undefined,
}
: undefined
}
/>
) : (
<div className="relative h-full w-full overflow-hidden">
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
<DocumentEditorWithRef
isSubmitting={isSubmitting}
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
value={pageDescription}
setShouldShowAlert={setShowAlert}
cancelUploadImage={fileService.cancelUpload}
ref={editorRef}
debouncedUpdatesEnabled={false}
setIsSubmitting={setIsSubmitting}
updatePageTitle={updatePageTitle}
onActionCompleteHandler={actionCompleteAlert}
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
onChange={(_description_json: Object, description_html: string) => {
setShowAlert(true);
onChange(description_html);
handleSubmit(updatePage)();
}}
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
is_archived: archived_at ? true : false,
action: archived_at ? unArchivePage : archivePage,
}
: undefined
}
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
/>
)}
<>
<PageHead title={pageTitle} />
<div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden">
{isPageReadOnly ? (
<DocumentReadOnlyEditorWithRef
onActionCompleteHandler={actionCompleteAlert}
ref={editorRef}
value={pageDescription}
customClassName={"tracking-tight w-full px-0"}
borderOnFocus={false}
noBorder
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: archived_at ? new Date(archived_at) : undefined,
}
: undefined
}
/>
{projectId && envConfig?.has_openai_configured && (
<div className="absolute right-[68px] top-2.5">
<GptAssistantPopover
isOpen={gptModalOpen}
projectId={projectId.toString()}
handleClose={() => {
setGptModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptModal((prevData) => !prevData)}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
className="!min-w-[38rem]"
/>
</div>
)}
</div>
)}
<IssuePeekOverview />
) : (
<div className="relative h-full w-full overflow-hidden">
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
<DocumentEditorWithRef
isSubmitting={isSubmitting}
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
value={pageDescription}
setShouldShowAlert={setShowAlert}
cancelUploadImage={fileService.cancelUpload}
ref={editorRef}
debouncedUpdatesEnabled={false}
setIsSubmitting={setIsSubmitting}
updatePageTitle={updatePageTitle}
onActionCompleteHandler={actionCompleteAlert}
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
onChange={(_description_json: Object, description_html: string) => {
setShowAlert(true);
onChange(description_html);
handleSubmit(updatePage)();
}}
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
is_archived: archived_at ? true : false,
action: archived_at ? unArchivePage : archivePage,
}
: undefined
}
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
/>
)}
/>
{projectId && envConfig?.has_openai_configured && (
<div className="absolute right-[68px] top-2.5">
<GptAssistantPopover
isOpen={gptModalOpen}
projectId={projectId.toString()}
handleClose={() => {
setGptModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptModal((prevData) => !prevData)}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
className="!min-w-[38rem]"
/>
</div>
)}
</div>
)}
<IssuePeekOverview />
</div>
</div>
</div>
</>
) : (
<div className="grid h-full w-full place-items-center">
<Spinner />

View file

@ -6,7 +6,7 @@ import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useEventTracker, useUser } from "hooks/store";
import { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
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 { EUserWorkspaceRoles } from "constants/workspace";
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), {
ssr: false,
@ -63,7 +64,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
commandPalette: { toggleCreatePageModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { getProjectById } = useProject();
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
useProjectPages();
// hooks
@ -101,10 +102,12 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}
};
// derived values
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
const MobileTabList = () => (
<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 (
<>
<PageHead title={pageTitle} />
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
<>
{workspaceSlug && projectId && (

View file

@ -10,6 +10,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import useToast from "hooks/use-toast";
// components
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers";
// types
import { NextPageWithLayout } from "lib/types";
@ -41,16 +42,21 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
});
};
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
return (
<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">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
</section>
<>
<PageHead title={pageTitle} />
<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">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
</section>
</>
);
});

View file

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

View file

@ -1,41 +1,48 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react";
// hooks
import { useUser } from "hooks/store";
import { useProject, useUser } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout";
// components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers";
import { ProjectFeaturesList } from "components/project";
// types
import { NextPageWithLayout } from "lib/types";
const FeaturesSettingsPage: NextPageWithLayout = () => {
const FeaturesSettingsPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const {
membership: { fetchUserProjectInfo },
} = useUser();
const { currentProjectDetails } = useProject();
// fetch the project details
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
);
// derived values
const isAdmin = memberDetails?.role === 20;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
return (
<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">
<h3 className="text-xl font-medium">Features</h3>
</div>
<ProjectFeaturesList />
</section>
<>
<PageHead title={pageTitle} />
<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">
<h3 className="text-xl font-medium">Features</h3>
</div>
<ProjectFeaturesList />
</section>
</>
);
};
});
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

@ -2,6 +2,7 @@ import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { useTheme } from "next-themes";
import { observer } from "mobx-react";
// hooks
import { useUser } from "hooks/store";
// layouts
@ -11,6 +12,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
import { IntegrationService } from "services/integrations";
import { ProjectService } from "services/project";
// components
import { PageHead } from "components/core";
import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
@ -27,63 +29,66 @@ import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
const integrationService = new IntegrationService();
const projectService = new ProjectService();
const ProjectIntegrationsPage: NextPageWithLayout = () => {
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
// fetch project details
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
// fetch Integrations list
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
);
// derived values
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
const isAdmin = projectDetails?.member_role === 20;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
return (
<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">
<h3 className="text-xl font-medium">Integrations</h3>
</div>
{workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<div>
{workspaceIntegrations.map((integration) => (
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
))}
</div>
<>
<PageHead title={pageTitle} />
<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">
<h3 className="text-xl font-medium">Integrations</h3>
</div>
{workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<div>
{workspaceIntegrations.map((integration) => (
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
))}
</div>
) : (
<div className="h-full w-full py-8">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
size="lg"
disabled={!isAdmin}
/>
</div>
)
) : (
<div className="h-full w-full py-8">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
size="lg"
disabled={!isAdmin}
/>
</div>
)
) : (
<IntegrationsSettingsLoader />
)}
</div>
<IntegrationsSettingsLoader />
)}
</div>
</>
);
};
});
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

@ -1,19 +1,33 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
// layouts
import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout";
// components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers";
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project";
// types
import { NextPageWithLayout } from "lib/types";
// hooks
import { useProject } from "hooks/store";
const MembersSettingsPage: NextPageWithLayout = () => (
<section className={`w-full overflow-y-auto py-8 pr-9`}>
<ProjectSettingsMemberDefaults />
<ProjectMemberList />
</section>
);
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`}>
<ProjectSettingsMemberDefaults />
<ProjectMemberList />
</section>
</>
);
});
MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
return (

View file

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

View file

@ -1,13 +1,34 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// components
import { ProjectViewsHeader } from "components/headers";
import { ProjectViewsList } from "components/views";
import { PageHead } from "components/core";
// hooks
import { useProject } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
// 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) {
return (

View file

@ -1,13 +1,28 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
// components
import { PageHead } from "components/core";
import { ProjectCardList } from "components/project";
import { ProjectsHeader } from "components/headers";
// layouts
import { AppLayout } from "layouts/app-layout";
// type
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) {
return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;

View file

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

View file

@ -1,11 +1,12 @@
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import { useUser, useWorkspace } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// component
import { WorkspaceSettingHeader } from "components/headers";
import { PageHead } from "components/core";
// ui
import { Button } from "@plane/ui";
// types
@ -18,33 +19,41 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
if (!isAdmin)
return (
<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>
</div>
<>
<PageHead title={pageTitle} />
<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>
</div>
</>
);
return (
<section className="w-full overflow-y-auto py-8 pr-9">
<div>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Billing & Plans</h3>
</div>
</div>
<div className="px-4 py-6">
<>
<PageHead title={pageTitle} />
<section className="w-full overflow-y-auto py-8 pr-9">
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<Button variant="neutral-primary">View Plans</Button>
</a>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Billing & Plans</h3>
</div>
</div>
</div>
</section>
<div className="px-4 py-6">
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<Button variant="neutral-primary">View Plans</Button>
</a>
</div>
</div>
</section>
</>
);
});

View file

@ -1,12 +1,13 @@
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import { useUser, useWorkspace } from "hooks/store";
// layout
import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components
import { WorkspaceSettingHeader } from "components/headers";
import ExportGuide from "components/exporter/guide";
import { PageHead } from "components/core";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -17,24 +18,33 @@ const ExportsPage: NextPageWithLayout = observer(() => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const hasPageAccess =
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
if (!hasPageAccess)
return (
<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>
</div>
<>
<PageHead title={pageTitle} />
<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>
</div>
</>
);
return (
<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">
<h3 className="text-xl font-medium">Exports</h3>
<>
<PageHead title={pageTitle} />
<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">
<h3 className="text-xl font-medium">Exports</h3>
</div>
<ExportGuide />
</div>
<ExportGuide />
</div>
</>
);
});

View file

@ -1,12 +1,13 @@
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import { useUser, useWorkspace } from "hooks/store";
// layouts
import { WorkspaceSettingLayout } from "layouts/settings-layout";
import { AppLayout } from "layouts/app-layout";
// components
import IntegrationGuide from "components/integration/guide";
import { WorkspaceSettingHeader } from "components/headers";
import { PageHead } from "components/core";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -17,23 +18,32 @@ const ImportsPage: NextPageWithLayout = observer(() => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
if (!isAdmin)
return (
<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>
</div>
<>
<PageHead title={pageTitle} />
<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>
</div>
</>
);
return (
<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">
<h3 className="text-xl font-medium">Imports</h3>
</div>
<IntegrationGuide />
</section>
<>
<PageHead title={pageTitle} />
<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">
<h3 className="text-xl font-medium">Imports</h3>
</div>
<IntegrationGuide />
</section>
</>
);
});

View file

@ -1,14 +1,30 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
// layouts
import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// hooks
import { useWorkspace } from "hooks/store";
// components
import { WorkspaceSettingHeader } from "components/headers";
import { WorkspaceDetails } from "components/workspace";
import { PageHead } from "components/core";
// 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) {
return (

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useUser, useWebhook } from "hooks/store";
import { useUser, useWebhook, useWorkspace } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
// components
import { WorkspaceSettingHeader } from "components/headers";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
import { PageHead } from "components/core";
// ui
import { Spinner } from "@plane/ui";
// types
@ -29,6 +30,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
// toast
const { setToastAlert } = useToast();
@ -38,6 +40,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
// }, [clearSecretKey, isCreated]);
const isAdmin = currentWorkspaceRole === 20;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
useSWR(
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
@ -76,9 +79,12 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
if (!isAdmin)
return (
<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>
</div>
<>
<PageHead title={pageTitle} />
<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>
</div>
</>
);
if (!currentWebhook)
@ -90,6 +96,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
<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";
// constants
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { PageHead } from "components/core";
const WebhooksListPage: NextPageWithLayout = observer(() => {
// states
@ -47,6 +48,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
// clear secret key when modal is closed.
useEffect(() => {
@ -55,53 +57,59 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
if (!isAdmin)
return (
<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>
</div>
<>
<PageHead title={pageTitle} />
<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>
</div>
</>
);
if (!webhooks) return <WebhookSettingsLoader />;
return (
<div className="h-full w-full overflow-hidden py-8 pr-9">
<CreateWebhookModal
createWebhook={createWebhook}
clearSecretKey={clearSecretKey}
currentWorkspace={currentWorkspace}
isOpen={showCreateWebhookModal}
onClose={() => {
setShowCreateWebhookModal(false);
}}
/>
{Object.keys(webhooks).length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
<>
<PageHead title={pageTitle} />
<div className="h-full w-full overflow-hidden py-8 pr-9">
<CreateWebhookModal
createWebhook={createWebhook}
clearSecretKey={clearSecretKey}
currentWorkspace={currentWorkspace}
isOpen={showCreateWebhookModal}
onClose={() => {
setShowCreateWebhookModal(false);
}}
/>
{Object.keys(webhooks).length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
</div>
<WebhooksList />
</div>
<WebhooksList />
</div>
) : (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
) : (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
</div>
)}
</div>
)}
</div>
</>
);
});

View file

@ -7,15 +7,26 @@ import { AllIssueLayoutRoot } from "components/issues";
import { GlobalIssuesHeader } from "components/headers";
// types
import { NextPageWithLayout } from "lib/types";
import { observer } from "mobx-react";
import { useWorkspace } from "hooks/store";
import { PageHead } from "components/core";
const GlobalViewIssuesPage: NextPageWithLayout = () => (
<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">
<GlobalViewsHeader />
<AllIssueLayoutRoot />
</div>
</div>
);
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="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<AllIssueLayoutRoot />
</div>
</div>
</>
);
});
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;

View file

@ -1,7 +1,9 @@
import React, { useState, ReactElement } from "react";
import { observer } from "mobx-react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { PageHead } from "components/core";
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
// ui
@ -12,31 +14,40 @@ import { Search } from "lucide-react";
import { NextPageWithLayout } from "lib/types";
// constants
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("");
// store
const { currentWorkspace } = useWorkspace();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
return (
<div className="flex flex-col">
<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">
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
<Input
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
mode="true-transparent"
/>
<>
<PageHead title={pageTitle} />
<div className="flex flex-col">
<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">
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
<Input
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
mode="true-transparent"
/>
</div>
</div>
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div>
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div>
</>
);
};
});
WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;