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,19 +35,22 @@ export const SignInView = observer(() => {
); );
return ( return (
<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"> <PageHead title="Sign In" />
<div className="flex items-center gap-x-2 py-10"> <div className="h-full w-full bg-onboarding-gradient-100">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" /> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div> </div>
</div>
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3"> <div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0"> <div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
<SignInRoot /> <SignInRoot />
</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,21 +43,24 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
}); });
return ( return (
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9"> <>
<ProfileStats userProfile={userProfile} /> <PageHead title="Profile - Summary" />
<ProfileWorkload stateDistribution={stateDistribution} /> <div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2"> <ProfileStats userProfile={userProfile} />
<ProfilePriorityDistribution userProfile={userProfile} /> <ProfileWorkload stateDistribution={stateDistribution} />
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} /> <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> </div>
<ProfileActivity /> </>
</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,38 +1,51 @@
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 (
<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"> <PageHead title={pageTitle} />
<button <div className="flex h-full w-full flex-col">
type="button" <div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)} <button
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs" type="button"
> onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
<ArchiveIcon className="h-4 w-4" /> className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
<span>Archived Issues</span> >
<X className="h-3 w-3" /> <ArchiveIcon className="h-4 w-4" />
</button> <span>Archived Issues</span>
<X className="h-3 w-3" />
</button>
</div>
<ArchivedIssueLayoutRoot />
</div> </div>
<ArchivedIssueLayoutRoot /> </>
</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,143 +77,146 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<div className="w-full h-full"> <>
<CycleCreateUpdateModal <PageHead title={pageTitle} />
workspaceSlug={workspaceSlug.toString()} <div className="w-full h-full">
projectId={projectId.toString()} <CycleCreateUpdateModal
isOpen={createModal} workspaceSlug={workspaceSlug.toString()}
handleClose={() => setCreateModal(false)} projectId={projectId.toString()}
/> isOpen={createModal}
{totalCycles === 0 ? ( handleClose={() => setCreateModal(false)}
<div className="h-full place-items-center"> />
<EmptyState {totalCycles === 0 ? (
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title} <div className="h-full place-items-center">
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description} <EmptyState
image={EmptyStateImagePath} title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
comicBox={{ description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title, image={EmptyStateImagePath}
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description, comicBox={{
}} title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
primaryButton={{ description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text, }}
onClick: () => { primaryButton={{
setTrackElement("Cycle empty state"); text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
setCreateModal(true); onClick: () => {
}, setTrackElement("Cycle empty state");
}} setCreateModal(true);
size="lg" },
disabled={!isEditingAllowed} }}
/> size="lg"
</div> disabled={!isEditingAllowed}
) : ( />
<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>
</div> </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}> return (
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tooltip key={layout.key} tooltipContent={layout.title}>
{cycleTab && cycleLayout && ( <button
<CyclesView type="button"
filter="all" className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
layout={cycleLayout} cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
workspaceSlug={workspaceSlug.toString()} }`}
projectId={projectId.toString()} onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
peekCycle={peekCycle?.toString()} >
/> <layout.icon
)} strokeWidth={2}
</Tab.Panel> 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"> <Tab.Panels as={Fragment}>
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> <Tab.Panel as="div" className="h-full overflow-y-auto">
</Tab.Panel> {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"> <Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
{cycleTab && cycleLayout && ( <ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
<CyclesView </Tab.Panel>
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"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && ( {cycleTab && cycleLayout && (
<CyclesView <CyclesView
filter="completed" filter="upcoming"
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
peekCycle={peekCycle?.toString()} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">
{cycleTab && cycleLayout && workspaceSlug && projectId && ( {cycleTab && cycleLayout && workspaceSlug && projectId && (
<CyclesView <CyclesView
filter="draft" filter="completed"
layout={cycleLayout as TCycleLayout} layout={cycleLayout as TCycleLayout}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
peekCycle={peekCycle?.toString()} peekCycle={peekCycle?.toString()}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
</Tab.Panels>
</Tab.Group> <Tab.Panel as="div" className="h-full overflow-y-auto">
)} {cycleTab && cycleLayout && workspaceSlug && projectId && (
</div> <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"; 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 (
<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"> <PageHead title={pageTitle} />
<button <div className="flex h-full w-full flex-col">
type="button" <div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)} <button
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs" type="button"
> onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
<PenSquare className="h-4 w-4" /> className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
<span>Draft Issues</span> >
<PenSquare className="h-4 w-4" />
<span>Draft Issues</span>
</button>
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</button> </div>
<DraftIssueLayoutRoot />
</div> </div>
<DraftIssueLayoutRoot /> </>
</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,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 <></>; if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
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"> <PageHead title={pageTitle} />
<InboxSidebarRoot <div className="relative flex h-full overflow-hidden">
workspaceSlug={workspaceSlug.toString()} <div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
projectId={projectId.toString()} <InboxSidebarRoot
inboxId={inboxId.toString()} 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>
<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 // 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(() => {
<div className="h-full w-full"> const router = useRouter();
<ProjectLayoutRoot /> const { projectId } = router.query;
</div> // 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) { 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,113 +256,116 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return pageIdMobx ? ( return pageIdMobx ? (
<div className="flex h-full flex-col justify-between"> <>
<div className="h-full w-full overflow-hidden"> <PageHead title={pageTitle} />
{isPageReadOnly ? ( <div className="flex h-full flex-col justify-between">
<DocumentReadOnlyEditorWithRef <div className="h-full w-full overflow-hidden">
onActionCompleteHandler={actionCompleteAlert} {isPageReadOnly ? (
ref={editorRef} <DocumentReadOnlyEditorWithRef
value={pageDescription} onActionCompleteHandler={actionCompleteAlert}
customClassName={"tracking-tight w-full px-0"} ref={editorRef}
borderOnFocus={false} value={pageDescription}
noBorder customClassName={"tracking-tight w-full px-0"}
documentDetails={{ borderOnFocus={false}
title: pageTitle, noBorder
created_by: created_by, documentDetails={{
created_on: created_at, title: pageTitle,
last_updated_at: updated_at, created_by: created_by,
last_updated_by: updated_by, created_on: created_at,
}} last_updated_at: updated_at,
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined} last_updated_by: updated_by,
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined} }}
pageArchiveConfig={ pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
userCanArchive pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
? { pageArchiveConfig={
action: archived_at ? unArchivePage : archivePage, userCanArchive
is_archived: archived_at ? true : false, ? {
archived_at: archived_at ? new Date(archived_at) : undefined, action: archived_at ? unArchivePage : archivePage,
} is_archived: archived_at ? true : false,
: undefined 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}
/>
)}
/> />
{projectId && envConfig?.has_openai_configured && ( ) : (
<div className="absolute right-[68px] top-2.5"> <div className="relative h-full w-full overflow-hidden">
<GptAssistantPopover <Controller
isOpen={gptModalOpen} name="description_html"
projectId={projectId.toString()} control={control}
handleClose={() => { render={({ field: { onChange } }) => (
setGptModal((prevData) => !prevData); <DocumentEditorWithRef
// this is done so that the title do not reset after gpt popover closed isSubmitting={isSubmitting}
reset(getValues()); documentDetails={{
}} title: pageTitle,
onResponse={(response) => { created_by: created_by,
handleAiAssistance(response); created_on: created_at,
}} last_updated_at: updated_at,
placement="top-end" last_updated_by: updated_by,
button={ }}
<button uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
type="button" deleteFile={fileService.getDeleteImageFunction(workspaceId)}
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90" restoreFile={fileService.getRestoreImageFunction(workspaceId)}
onClick={() => setGptModal((prevData) => !prevData)} value={pageDescription}
> setShouldShowAlert={setShowAlert}
<Sparkle className="h-4 w-4" /> cancelUploadImage={fileService.cancelUpload}
AI ref={editorRef}
</button> debouncedUpdatesEnabled={false}
} setIsSubmitting={setIsSubmitting}
className="!min-w-[38rem]" updatePageTitle={updatePageTitle}
/> onActionCompleteHandler={actionCompleteAlert}
</div> customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
)} onChange={(_description_json: Object, description_html: string) => {
</div> setShowAlert(true);
)} onChange(description_html);
<IssuePeekOverview /> 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> </>
) : ( ) : (
<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,16 +42,21 @@ 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 (
<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"> <PageHead title={pageTitle} />
<h3 className="text-xl font-medium">Automations</h3> <section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
</div> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<AutoArchiveAutomation handleChange={handleChange} /> <h3 className="text-xl font-medium">Automations</h3>
<AutoCloseAutomation handleChange={handleChange} /> </div>
</section> <AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
</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 (
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}> <>
<EstimatesList /> <PageHead title={pageTitle} />
</div> <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 { 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 (
<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"> <PageHead title={pageTitle} />
<h3 className="text-xl font-medium">Features</h3> <section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
</div> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<ProjectFeaturesList /> <h3 className="text-xl font-medium">Features</h3>
</section> </div>
<ProjectFeaturesList />
</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,63 +29,66 @@ 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 (
<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"> <PageHead title={pageTitle} />
<h3 className="text-xl font-medium">Integrations</h3> <div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
</div> <div className="flex items-center border-b border-custom-border-100 py-3.5">
{workspaceIntegrations ? ( <h3 className="text-xl font-medium">Integrations</h3>
workspaceIntegrations.length > 0 ? ( </div>
<div> {workspaceIntegrations ? (
{workspaceIntegrations.map((integration) => ( workspaceIntegrations.length > 0 ? (
<IntegrationCard key={integration.integration_detail.id} integration={integration} /> <div>
))} {workspaceIntegrations.map((integration) => (
</div> <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"> <IntegrationsSettingsLoader />
<EmptyState )}
title={emptyStateDetail.title} </div>
description={emptyStateDetail.description} </>
image={emptyStateImage}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
size="lg"
disabled={!isAdmin}
/>
</div>
)
) : (
<IntegrationsSettingsLoader />
)}
</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(() => {
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9"> const { currentProjectDetails } = useProject();
<ProjectSettingsLabelList /> const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
</div>
); 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) { 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(() => {
<section className={`w-full overflow-y-auto py-8 pr-9`}> // store
<ProjectSettingsMemberDefaults /> const { currentProjectDetails } = useProject();
<ProjectMemberList /> // derived values
</section> 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) { 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 = () => {
}} }}
/> />
) : ( ) : (
<ProjectViewLayoutRoot /> <>
<PageHead title={pageTitle} />
<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 (
<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> <PageHead title={pageTitle} />
</div> <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) { 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,33 +19,41 @@ 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 (
<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> <PageHead title={pageTitle} />
</div> <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 ( return (
<section className="w-full overflow-y-auto py-8 pr-9"> <>
<div> <PageHead title={pageTitle} />
<div className="flex items-center border-b border-custom-border-100 py-3.5"> <section className="w-full overflow-y-auto py-8 pr-9">
<h3 className="text-xl font-medium">Billing & Plans</h3>
</div>
</div>
<div className="px-4 py-6">
<div> <div>
<h4 className="text-md mb-1 leading-6">Current plan</h4> <div className="flex items-center border-b border-custom-border-100 py-3.5">
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p> <h3 className="text-xl font-medium">Billing & Plans</h3>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer"> </div>
<Button variant="neutral-primary">View Plans</Button>
</a>
</div> </div>
</div> <div className="px-4 py-6">
</section> <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"; 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 (
<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> <PageHead title={pageTitle} />
</div> <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 ( 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"> <PageHead title={pageTitle} />
<h3 className="text-xl font-medium">Exports</h3> <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> </div>
<ExportGuide /> </>
</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 (
<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> <PageHead title={pageTitle} />
</div> <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 ( 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"> <PageHead title={pageTitle} />
<h3 className="text-xl font-medium">Imports</h3> <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">
<IntegrationGuide /> <h3 className="text-xl font-medium">Imports</h3>
</section> </div>
<IntegrationGuide />
</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 (
<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> <PageHead title={pageTitle} />
</div> <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, () => const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
@ -46,16 +53,21 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<section className="w-full overflow-y-auto py-8 pr-9"> <>
<IntegrationAndImportExportBanner bannerName="Integrations" /> <PageHead title={pageTitle} />
<div> <section className="w-full overflow-y-auto py-8 pr-9">
{appIntegrations ? ( <IntegrationAndImportExportBanner bannerName="Integrations" />
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />) <div>
) : ( {appIntegrations ? (
<IntegrationsSettingsLoader /> appIntegrations.map((integration) => (
)} <SingleIntegrationCard key={integration.id} integration={integration} />
</div> ))
</section> ) : (
<IntegrationsSettingsLoader />
)}
</div>
</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 (
<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> <PageHead title={pageTitle} />
</div> <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) 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,53 +57,59 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
if (!isAdmin) if (!isAdmin)
return ( 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> <PageHead title={pageTitle} />
</div> <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 />; if (!webhooks) return <WebhookSettingsLoader />;
return ( return (
<div className="h-full w-full overflow-hidden py-8 pr-9"> <>
<CreateWebhookModal <PageHead title={pageTitle} />
createWebhook={createWebhook} <div className="h-full w-full overflow-hidden py-8 pr-9">
clearSecretKey={clearSecretKey} <CreateWebhookModal
currentWorkspace={currentWorkspace} createWebhook={createWebhook}
isOpen={showCreateWebhookModal} clearSecretKey={clearSecretKey}
onClose={() => { currentWorkspace={currentWorkspace}
setShowCreateWebhookModal(false); 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"> {Object.keys(webhooks).length > 0 ? (
<div className="text-xl font-medium">Webhooks</div> <div className="flex h-full w-full flex-col">
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}> <div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
Add webhook <div className="text-xl font-medium">Webhooks</div>
</Button> <Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
</div>
<WebhooksList />
</div> </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="flex h-full w-full flex-col"> <div className="text-xl font-medium">Webhooks</div>
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5"> <Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
<div className="text-xl font-medium">Webhooks</div> Add webhook
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}> </Button>
Add webhook </div>
</Button> <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 className="h-full w-full flex items-center justify-center"> )}
<EmptyState </div>
title={emptyStateDetail.title} </>
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</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(() => {
<div className="h-full overflow-hidden bg-custom-background-100"> const { currentWorkspace } = useWorkspace();
<div className="flex h-full w-full flex-col border-b border-custom-border-300"> // derived values
<GlobalViewsHeader /> const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined;
<AllIssueLayoutRoot /> return (
</div> <>
</div> <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) { 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,31 +14,40 @@ 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 (
<div className="flex flex-col"> <>
<div className="flex h-full w-full flex-col overflow-hidden"> <PageHead title={pageTitle} />
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3"> <div className="flex flex-col">
<Search className="text-custom-text-200" size={14} strokeWidth={2} /> <div className="flex h-full w-full flex-col overflow-hidden">
<Input <div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none" <Search className="text-custom-text-200" size={14} strokeWidth={2} />
value={query} <Input
onChange={(e) => setQuery(e.target.value)} className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search" value={query}
mode="true-transparent" onChange={(e) => setQuery(e.target.value)}
/> placeholder="Search"
mode="true-transparent"
/>
</div>
</div> </div>
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div> </div>
{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) { 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,59 +86,62 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
}; };
return ( return (
<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 "> <PageHead title="Forgot Password" />
<div className="flex items-center gap-x-2 py-10"> <div className="h-full w-full bg-onboarding-gradient-100">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" /> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <div className="flex items-center gap-x-2 py-10">
</div> <Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
</div> <span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 "> </div>
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96"> <div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100"> <div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
Get on your flight deck <div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
</h1> <h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">Get a link to reset your password</p> Get on your flight deck
<form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 sm:w-96"> </h1>
<Controller <p className="mt-2.5 text-center text-sm text-onboarding-text-200">Get a link to reset your password</p>
control={control} <form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 sm:w-96">
name="email" <Controller
rules={{ control={control}
required: "Email is required", name="email"
validate: (value) => checkEmailValidity(value) || "Email is invalid", rules={{
}} required: "Email is required",
render={({ field: { value, onChange, ref } }) => ( validate: (value) => checkEmailValidity(value) || "Email is invalid",
<Input }}
id="email" render={({ field: { value, onChange, ref } }) => (
name="email" <Input
type="email" id="email"
value={value} name="email"
onChange={onChange} type="email"
ref={ref} value={value}
hasError={Boolean(errors.email)} onChange={onChange}
placeholder="name@company.com" ref={ref}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" hasError={Boolean(errors.email)}
/> placeholder="name@company.com"
)} className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/> />
<Button )}
type="submit" />
variant="primary" <Button
className="w-full" type="submit"
size="xl" variant="primary"
disabled={!isValid} className="w-full"
loading={isSubmitting || resendTimerCode > 0} size="xl"
> disabled={!isValid}
{resendTimerCode > 0 ? `Request new link in ${resendTimerCode}s` : "Get link"} loading={isSubmitting || resendTimerCode > 0}
</Button> >
</form> {resendTimerCode > 0 ? `Request new link in ${resendTimerCode}s` : "Get link"}
</Button>
</form>
</div>
<LatestFeatureBlock />
</div> </div>
<LatestFeatureBlock />
</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,90 +91,93 @@ const ResetPasswordPage: NextPageWithLayout = () => {
}; };
return ( return (
<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 "> <PageHead title="Reset Password" />
<div className="flex items-center gap-x-2 py-10"> <div className="h-full w-full bg-onboarding-gradient-100">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" /> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div> </div>
</div>
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 "> <div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0"> <div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96"> <div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100"> <h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
Let{"'"}s get a new password Let{"'"}s get a new password
</h1> </h1>
<form onSubmit={handleSubmit(handleResetPassword)} className="mx-auto mt-11 space-y-4 sm:w-96"> <form onSubmit={handleSubmit(handleResetPassword)} className="mx-auto mt-11 space-y-4 sm:w-96">
<Controller <Controller
control={control} control={control}
name="email" name="email"
rules={{ rules={{
required: "Email is required", required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid", validate: (value) => checkEmailValidity(value) || "Email is invalid",
}} }}
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400"
disabled
/>
)}
/>
<Controller
control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input <Input
type={showPassword ? "text" : "password"} id="email"
name="email"
type="email"
value={value} value={value}
onChange={onChange} onChange={onChange}
hasError={Boolean(errors.password)} ref={ref}
placeholder="Enter password" hasError={Boolean(errors.email)}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" placeholder="name@company.com"
minLength={8} className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400"
disabled
/> />
{showPassword ? ( )}
<EyeOff />
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer" <Controller
onClick={() => setShowPassword(false)} control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
/> />
) : ( {showPassword ? (
<Eye <EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer" className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)} onClick={() => setShowPassword(false)}
/> />
)} ) : (
</div> <Eye
)} className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
/> onClick={() => setShowPassword(true)}
<Button />
type="submit" )}
variant="primary" </div>
className="w-full" )}
size="xl" />
disabled={!isValid} <Button
loading={isSubmitting} type="submit"
> variant="primary"
Set password className="w-full"
</Button> size="xl"
</form> disabled={!isValid}
loading={isSubmitting}
>
Set password
</Button>
</form>
</div>
<LatestFeatureBlock />
</div> </div>
<LatestFeatureBlock />
</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,20 +30,23 @@ const SignUpPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<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"> <PageHead title="Sign Up" />
<div className="flex items-center gap-x-2 py-10"> <div className="h-full w-full bg-onboarding-gradient-100">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" /> <div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<span className="text-2xl font-semibold sm:text-3xl">Plane</span> <div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div> </div>
</div>
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3"> <div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0"> <div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
<SignUpRoot /> <SignUpRoot />
</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,38 +38,41 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<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"> <PageHead title="Create Workspace" />
<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="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
<Link <div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 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" />
href="/" <Link
> className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"
<div className="h-[30px] w-[133px]"> href="/"
{theme === "light" ? ( >
<Image src={BlackHorizontalLogo} alt="Plane black logo" /> <div className="h-[30px] w-[133px]">
) : ( {theme === "light" ? (
<Image src={WhiteHorizontalLogo} alt="Plane white logo" /> <Image src={BlackHorizontalLogo} alt="Plane black logo" />
)} ) : (
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
</Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email}
</div> </div>
</Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email}
</div> </div>
</div> <div className="relative flex h-full justify-center px-8 pb-8 sm:w-10/12 sm:items-center sm:justify-start sm:p-0 sm:pr-[8.33%] md:w-9/12 lg:w-4/5">
<div className="relative flex h-full justify-center px-8 pb-8 sm:w-10/12 sm:items-center sm:justify-start sm:p-0 sm:pr-[8.33%] md:w-9/12 lg:w-4/5"> <div className="w-full space-y-7 sm:space-y-10">
<div className="w-full space-y-7 sm:space-y-10"> <h4 className="text-2xl font-semibold">Create your workspace</h4>
<h4 className="text-2xl font-semibold">Create your workspace</h4> <div className="sm:w-3/4 md:w-2/5">
<div className="sm:w-3/4 md:w-2/5"> <CreateWorkspaceForm
<CreateWorkspaceForm onSubmit={onSubmit}
onSubmit={onSubmit} defaultValues={defaultValues}
defaultValues={defaultValues} setDefaultValues={setDefaultValues}
setDefaultValues={setDefaultValues} />
/> </div>
</div> </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,37 +24,40 @@ const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<div className="flex flex-col gap-8"> <>
<div className="mb-2 border-b border-custom-border-100 pb-3"> <PageHead title="God Mode - AI" />
<div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div> <div className="flex flex-col gap-8">
<div className="text-sm font-normal text-custom-text-300"> <div className="mb-2 border-b border-custom-border-100 pb-3">
Configure your AI API credentials so Plane AI features are turned on for all your workspaces. <div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
<div className="text-sm font-normal text-custom-text-300">
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
</div>
</div> </div>
</div> {formattedConfig ? (
{formattedConfig ? ( <>
<> <div>
<div> <div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div> <div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div> </div>
</div> <InstanceAIForm config={formattedConfig} />
<InstanceAIForm config={formattedConfig} /> <div className="my-2 flex">
<div className="my-2 flex"> <div className="flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
<div className="flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200"> <Lightbulb height="14" width="14" />
<Lightbulb height="14" width="14" /> <div>If you have a preferred AI models vendor, please get in touch with us.</div>
<div>If you have a preferred AI models vendor, please get in touch with us.</div> </div>
</div>
</>
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div> </div>
</div>
</>
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" /> </Loader>
</div> )}
<Loader.Item height="50px" /> </div>
</Loader> </>
)}
</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,69 +65,71 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<div className="flex flex-col gap-8"> <>
<div className="mb-2 border-b border-custom-border-100 pb-3"> <PageHead title="God Mode - SSO and OAuth" />
<div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div> <div className="flex flex-col gap-8">
<div className="text-sm font-normal text-custom-text-300"> <div className="mb-2 border-b border-custom-border-100 pb-3">
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the <div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div>
settings. <div className="text-sm font-normal text-custom-text-300">
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the
settings.
</div>
</div> </div>
</div> {formattedConfig ? (
{formattedConfig ? ( <>
<> <div className="flex w-full flex-col gap-12 border-b border-custom-border-100 pb-8 lg:w-2/5">
<div className="flex w-full flex-col gap-12 border-b border-custom-border-100 pb-8 lg:w-2/5"> <div className="pointer-events-none mr-4 flex items-center gap-14 opacity-50">
<div className="pointer-events-none mr-4 flex items-center gap-14 opacity-50"> <div className="grow">
<div className="grow"> <div className="text-sm font-medium text-custom-text-100">
<div className="text-sm font-medium text-custom-text-100"> Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"}
Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"} </div>
<div className="text-xs font-normal text-custom-text-300">
<p>Slack-like emails for authentication.</p>
You need to have set up email{" "}
<Link href="email">
<span className="text-custom-primary-100 hover:underline">here</span>
</Link>{" "}
to enable this.
</div>
</div> </div>
<div className="text-xs font-normal text-custom-text-300"> <div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
<p>Slack-like emails for authentication.</p> <ToggleSwitch
You need to have set up email{" "} value={Boolean(parseInt(enableMagicLogin))}
<Link href="email"> // onChange={() => {
<span className="text-custom-primary-100 hover:underline">here</span> // Boolean(parseInt(enableMagicLogin)) === true
</Link>{" "} // ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
to enable this. // : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
// }}
onChange={() => {}}
size="sm"
disabled={isSubmitting}
/>
</div> </div>
</div> </div>
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}> <div className="mr-4 flex items-center gap-14">
<ToggleSwitch <div className="grow">
value={Boolean(parseInt(enableMagicLogin))} <div className="text-sm font-medium text-custom-text-100">
// onChange={() => { Let your users log in via the methods below
// Boolean(parseInt(enableMagicLogin)) === true </div>
// ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0") <div className="text-xs font-normal text-custom-text-300">
// : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1"); Toggling this off will disable all previous configs. Users will only be able to login with an e-mail
// }} and password combo.
onChange={() => {}} </div>
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
<div className="mr-4 flex items-center gap-14">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">
Let your users log in via the methods below
</div> </div>
<div className="text-xs font-normal text-custom-text-300"> <div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
Toggling this off will disable all previous configs. Users will only be able to login with an e-mail <ToggleSwitch
and password combo. value={Boolean(parseInt(enableSignup))}
onChange={() => {
Boolean(parseInt(enableSignup)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div> </div>
</div> </div>
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}> {/* <div className="flex items-center gap-14 mr-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignup))}
onChange={() => {
Boolean(parseInt(enableSignup)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
{/* <div className="flex items-center gap-14 mr-4">
<div className="grow"> <div className="grow">
<div className="text-custom-text-100 font-medium text-sm"> <div className="text-custom-text-100 font-medium text-sm">
Turn Email Password {Boolean(parseInt(enableEmailPassword)) ? "off" : "on"} Turn Email Password {Boolean(parseInt(enableEmailPassword)) ? "off" : "on"}
@ -146,36 +149,37 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
/> />
</div> </div>
</div> */} </div> */}
</div> </div>
<div className="flex flex-col gap-y-6 py-2"> <div className="flex flex-col gap-y-6 py-2">
<div className="w-full"> <div className="w-full">
<div className="flex items-center justify-between border-b border-custom-border-100 py-2"> <div className="flex items-center justify-between border-b border-custom-border-100 py-2">
<span className="text-lg font-medium tracking-tight">Google</span> <span className="text-lg font-medium tracking-tight">Google</span>
</div>
<div className="px-2 py-6">
<InstanceGoogleConfigForm config={formattedConfig} />
</div>
</div> </div>
<div className="px-2 py-6"> <div className="w-full">
<InstanceGoogleConfigForm config={formattedConfig} /> <div className="flex items-center justify-between border-b border-custom-border-100 py-2">
<span className="text-lg font-medium tracking-tight">Github</span>
</div>
<div className="px-2 py-6">
<InstanceGithubConfigForm config={formattedConfig} />
</div>
</div> </div>
</div> </div>
<div className="w-full"> </>
<div className="flex items-center justify-between border-b border-custom-border-100 py-2"> ) : (
<span className="text-lg font-medium tracking-tight">Github</span> <Loader className="space-y-4">
</div> <div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div className="px-2 py-6"> <Loader.Item height="50px" />
<InstanceGithubConfigForm config={formattedConfig} /> <Loader.Item height="50px" />
</div>
</div> </div>
</div>
</>
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
<Loader.Item height="50px" /> </Loader>
</div> )}
<Loader.Item height="50px" /> </div>
</Loader> </>
)}
</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,29 +22,32 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<div className="flex flex-col gap-8"> <>
<div className="mb-2 border-b border-custom-border-100 pb-3"> <PageHead title="God Mode - Email" />
<div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div> <div className="flex flex-col gap-8">
<div className="text-sm font-normal text-custom-text-300"> <div className="mb-2 border-b border-custom-border-100 pb-3">
Plane can send useful emails to you and your users from your own instance without talking to the Internet. <div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
</div> <div className="text-sm font-normal text-custom-text-300">
<div className="text-sm font-normal text-custom-text-300"> Plane can send useful emails to you and your users from your own instance without talking to the Internet.
Set it up below and please test your settings before you save them.{" "}
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div>
</div>
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div> </div>
<Loader.Item height="50px" /> <div className="text-sm font-normal text-custom-text-300">
</Loader> Set it up below and please test your settings before you save them.{" "}
)} <span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div> </div>
</div>
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
<Loader.Item height="50px" />
</Loader>
)}
</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,25 +22,28 @@ const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<div className="flex flex-col gap-8"> <>
<div className="mb-2 border-b border-custom-border-100 pb-3"> <PageHead title="God Mode - Images" />
<div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div> <div className="flex flex-col gap-8">
<div className="text-sm font-normal text-custom-text-300"> <div className="mb-2 border-b border-custom-border-100 pb-3">
Let your users search and choose images from third-party libraries <div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div>
</div> <div className="text-sm font-normal text-custom-text-300">
</div> Let your users search and choose images from third-party libraries
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div> </div>
<Loader.Item height="50px" /> </div>
</Loader> {formattedConfig ? (
)} <InstanceImageConfigForm config={formattedConfig} />
</div> ) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
<Loader.Item height="50px" />
</Loader>
)}
</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,26 +23,29 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins()); useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
return ( return (
<div className="flex h-full w-full flex-col gap-8"> <>
<div className="mb-2 border-b border-custom-border-100 pb-3"> <PageHead title="God Mode - General Settings" />
<div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div> <div className="flex h-full w-full flex-col gap-8">
<div className="text-sm font-normal text-custom-text-300"> <div className="mb-2 border-b border-custom-border-100 pb-3">
Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you <div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div>
will find your license key here. <div className="text-sm font-normal text-custom-text-300">
</div> Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you
</div> will find your license key here.
{instance && instanceAdmins ? (
<InstanceGeneralForm instance={instance} instanceAdmins={instanceAdmins} />
) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div> </div>
<Loader.Item height="50px" /> </div>
</Loader> {instance && instanceAdmins ? (
)} <InstanceGeneralForm instance={instance} instanceAdmins={instanceAdmins} />
</div> ) : (
<Loader className="space-y-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
<Loader.Item height="50px" />
</Loader>
)}
</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,108 +126,111 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
}; };
return ( return (
<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"> <PageHead title="Invitations" />
<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="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
<div className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"> <div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="h-[30px] w-[133px]"> <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" />
{theme === "light" ? ( <div className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3">
<Image src={BlackHorizontalLogo} alt="Plane black logo" /> <div className="h-[30px] w-[133px]">
) : ( {theme === "light" ? (
<Image src={WhiteHorizontalLogo} alt="Plane white logo" /> <Image src={BlackHorizontalLogo} alt="Plane black logo" />
)} ) : (
</div> <Image src={WhiteHorizontalLogo} alt="Plane white logo" />
</div> )}
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email}
</div>
</div>
{invitations ? (
invitations.length > 0 ? (
<div className="relative flex h-full justify-center px-8 pb-8 sm:w-10/12 sm:items-center sm:justify-start sm:p-0 sm:pr-[8.33%] md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="max-h-[37vh] space-y-4 overflow-y-auto md:w-3/5">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 rounded border px-3.5 py-5 ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid h-9 w-9 place-items-center rounded">
{invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}
>
<CheckCircle2 className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<Button
variant="primary"
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
>
Accept & Join
</Button>
<Link href={`/${redirectWorkspaceSlug}`}>
<span>
<Button variant="neutral-primary" size="md">
Go Home
</Button>
</span>
</Link>
</div>
</div> </div>
</div> </div>
) : ( <div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
<div className="fixed left-0 top-0 grid h-full w-full place-items-center"> {currentUser?.email}
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
primaryButton={{
text: "Back to dashboard",
onClick: () => router.push("/"),
}}
/>
</div> </div>
) </div>
) : null} {invitations ? (
</div> invitations.length > 0 ? (
<div className="relative flex h-full justify-center px-8 pb-8 sm:w-10/12 sm:items-center sm:justify-start sm:p-0 sm:pr-[8.33%] md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="max-h-[37vh] space-y-4 overflow-y-auto md:w-3/5">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 rounded border px-3.5 py-5 ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid h-9 w-9 place-items-center rounded">
{invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}
>
<CheckCircle2 className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<Button
variant="primary"
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
>
Accept & Join
</Button>
<Link href={`/${redirectWorkspaceSlug}`}>
<span>
<Button variant="neutral-primary" size="md">
Go Home
</Button>
</span>
</Link>
</div>
</div>
</div>
) : (
<div className="fixed left-0 top-0 grid h-full w-full place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
primaryButton={{
text: "Back to dashboard",
onClick: () => router.push("/"),
}}
/>
</div>
)
) : null}
</div>
</>
); );
}); });

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,159 +32,162 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
return ( return (
<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"> <PageHead title="Profile - Activity" />
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} /> <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">
<h3 className="text-xl font-medium">Activity</h3> <div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
</div> <SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
{userActivity ? ( <h3 className="text-xl font-medium">Activity</h3>
<div className="flex h-full w-full flex-col gap-2 overflow-y-auto"> </div>
<ul role="list" className="-mb-4"> {userActivity ? (
{userActivity.results.map((activityItem: any) => { <div className="flex h-full w-full flex-col gap-2 overflow-y-auto">
if (activityItem.field === "comment") { <ul role="list" className="-mb-4">
return ( {userActivity.results.map((activityItem: any) => {
<div key={activityItem.id} className="mt-2"> if (activityItem.field === "comment") {
<div className="relative flex items-start space-x-3"> return (
<div className="relative px-1"> <div key={activityItem.id} className="mt-2">
{activityItem.field ? ( <div className="relative flex items-start space-x-3">
activityItem.new_value === "restore" && ( <div className="relative px-1">
<History className="h-3.5 w-3.5 text-custom-text-200" /> {activityItem.field ? (
) activityItem.new_value === "restore" && (
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( <History className="h-3.5 w-3.5 text-custom-text-200" />
<img )
src={activityItem.actor_detail.avatar} ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
alt={activityItem.actor_detail.display_name} <img
height={30} src={activityItem.actor_detail.avatar}
width={30} alt={activityItem.actor_detail.display_name}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white" height={30}
/> width={30}
) : ( className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
<div />
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`} ) : (
> <div
{activityItem.actor_detail.display_name?.charAt(0)} className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
</div> >
)} {activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> <span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
<MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" /> <MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" />
</span> </span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name + " Bot"
: activityItem.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {calculateTimeAgo(activityItem.created_at)}
</p>
</div> </div>
<div className="issue-comments-section p-0"> <div className="min-w-0 flex-1">
<RichReadOnlyEditor <div>
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value} <div className="text-xs">
customClassName="text-xs border border-custom-border-200 bg-custom-background-100" {activityItem.actor_detail.is_bot
noBorder ? activityItem.actor_detail.first_name + " Bot"
borderOnFocus={false} : activityItem.actor_detail.display_name}
/> </div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {calculateTimeAgo(activityItem.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
<RichReadOnlyEditor
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
noBorder
borderOnFocus={false}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> );
); }
}
const message = const message =
activityItem.verb === "created" && activityItem.verb === "created" &&
activityItem.field !== "cycles" && activityItem.field !== "cycles" &&
activityItem.field !== "modules" && activityItem.field !== "modules" &&
activityItem.field !== "attachment" && activityItem.field !== "attachment" &&
activityItem.field !== "link" && activityItem.field !== "link" &&
activityItem.field !== "estimate" && activityItem.field !== "estimate" &&
!activityItem.field ? ( !activityItem.field ? (
<span> <span>
created <IssueLink activity={activityItem} /> created <IssueLink activity={activityItem} />
</span> </span>
) : ( ) : (
<ActivityMessage activity={activityItem} showIssue /> <ActivityMessage activity={activityItem} showIssue />
); );
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="relative pb-1">
<div className="relative flex items-center space-x-2"> <div className="relative flex items-center space-x-2">
<> <>
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="flex h-6 w-6 items-center justify-center"> <div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? ( {activityItem.field ? (
activityItem.new_value === "restore" ? ( activityItem.new_value === "restore" ? (
<History className="h-5 w-5 text-custom-text-200" /> <History className="h-5 w-5 text-custom-text-200" />
) : (
<ActivityIcon activity={activityItem} />
)
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="h-full w-full rounded-full object-cover"
/>
) : ( ) : (
<ActivityIcon activity={activityItem} /> <div
) className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( >
<img {activityItem.actor_detail.display_name?.charAt(0)}
src={activityItem.actor_detail.avatar} </div>
alt={activityItem.actor_detail.display_name} )}
height={24} </div>
width={24}
className="h-full w-full rounded-full object-cover"
/>
) : (
<div
className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> <div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4"> <div className="flex gap-1 break-words text-sm text-custom-text-200">
<div className="flex gap-1 break-words text-sm text-custom-text-200"> {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( <span className="text-gray font-medium">Plane</span>
<span className="text-gray font-medium">Plane</span> ) : activityItem.actor_detail.is_bot ? (
) : activityItem.actor_detail.is_bot ? (
<span className="text-gray font-medium">
{activityItem.actor_detail.first_name} Bot
</span>
) : (
<Link
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
>
<span className="text-gray font-medium"> <span className="text-gray font-medium">
{currentUser?.id === activityItem.actor_detail.id {activityItem.actor_detail.first_name} Bot
? "You"
: activityItem.actor_detail.display_name}
</span> </span>
</Link> ) : (
)}{" "} <Link
<div className="flex gap-1 truncate"> href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
{message}{" "} >
<span className="flex-shrink-0 whitespace-nowrap"> <span className="text-gray font-medium">
{calculateTimeAgo(activityItem.created_at)} {currentUser?.id === activityItem.actor_detail.id
</span> ? "You"
: activityItem.actor_detail.display_name}
</span>
</Link>
)}{" "}
<div className="flex gap-1 truncate">
{message}{" "}
<span className="flex-shrink-0 whitespace-nowrap">
{calculateTimeAgo(activityItem.created_at)}
</span>
</div>
</div> </div>
</div> </div>
</div> </>
</> </div>
</div> </div>
</div> </li>
</li> );
); }
} })}
})} </ul>
</ul> </div>
</div> ) : (
) : ( <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,93 +90,98 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<div className="flex flex-col h-full"> <>
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4"> <PageHead title="Profile - Change Password" />
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} /> <div className="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
</div>
<form
onSubmit={handleSubmit(handleChangePassword)}
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
>
<h3 className="text-xl font-medium">Change password</h3>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Current password</h4>
<Controller
control={control}
name="old_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="old_password"
type="password"
value={value}
onChange={onChange}
placeholder="Old password"
className="w-full rounded-md font-medium"
hasError={Boolean(errors.old_password)}
/>
)}
/>
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">New password</h4>
<Controller
control={control}
name="new_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="new_password"
type="password"
value={value}
placeholder="New password"
onChange={onChange}
className="w-full"
hasError={Boolean(errors.new_password)}
/>
)}
/>
{errors.new_password && <span className="text-xs text-red-500">{errors.new_password.message}</span>}
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Confirm password</h4>
<Controller
control={control}
name="confirm_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="confirm_password"
type="password"
placeholder="Confirm password"
value={value}
onChange={onChange}
className="w-full"
hasError={Boolean(errors.confirm_password)}
/>
)}
/>
{errors.confirm_password && (
<span className="text-xs text-red-500">{errors.confirm_password.message}</span>
)}
</div>
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Changing password..." : "Change password"}
</Button>
</div>
</form>
</div> </div>
<form </>
onSubmit={handleSubmit(handleChangePassword)}
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
>
<h3 className="text-xl font-medium">Change password</h3>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Current password</h4>
<Controller
control={control}
name="old_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="old_password"
type="password"
value={value}
onChange={onChange}
placeholder="Old password"
className="w-full rounded-md font-medium"
hasError={Boolean(errors.old_password)}
/>
)}
/>
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">New password</h4>
<Controller
control={control}
name="new_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="new_password"
type="password"
value={value}
placeholder="New password"
onChange={onChange}
className="w-full"
hasError={Boolean(errors.new_password)}
/>
)}
/>
{errors.new_password && <span className="text-xs text-red-500">{errors.new_password.message}</span>}
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Confirm password</h4>
<Controller
control={control}
name="confirm_password"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<Input
id="confirm_password"
type="password"
placeholder="Confirm password"
value={value}
onChange={onChange}
className="w-full"
hasError={Boolean(errors.confirm_password)}
/>
)}
/>
{errors.confirm_password && <span className="text-xs text-red-500">{errors.confirm_password.message}</span>}
</div>
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Changing password..." : "Change password"}
</Button>
</div>
</form>
</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,8 +297,9 @@ 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 (
<div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8"> <>
<EmailNotificationForm data={data} /> <PageHead title="Profile - Email Preference" />
</div> <div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
<EmailNotificationForm data={data} />
</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">