Merge branch 'develop' of gurusainath:makeplane/plane into perf/cycle-module-endpoints

This commit is contained in:
gurusainath 2024-02-20 14:12:01 +05:30
commit e65db66836
82 changed files with 1969 additions and 1375 deletions

View File

@ -1,6 +1,7 @@
# Django imports # Django imports
from django.db.models import Count, Sum, F, Q from django.db.models import Count, Sum, F, Q
from django.db.models.functions import ExtractMonth from django.db.models.functions import ExtractMonth
from django.utils import timezone
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -331,8 +332,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
.order_by("state_group") .order_by("state_group")
) )
current_year = timezone.now().year
issue_completed_month_wise = ( issue_completed_month_wise = (
base_issues.filter(completed_at__isnull=False) base_issues.filter(completed_at__year=current_year)
.annotate(month=ExtractMonth("completed_at")) .annotate(month=ExtractMonth("completed_at"))
.values("month") .values("month")
.annotate(count=Count("*")) .annotate(count=Count("*"))

View File

@ -15,6 +15,7 @@ import { EditorBubbleMenu } from "src/ui/menus/bubble-menu";
export type IRichTextEditor = { export type IRichTextEditor = {
value: string; value: string;
initialValue?: string;
dragDropEnabled?: boolean; dragDropEnabled?: boolean;
uploadFile: UploadImage; uploadFile: UploadImage;
restoreFile: RestoreImage; restoreFile: RestoreImage;
@ -54,6 +55,7 @@ const RichTextEditor = ({
setShouldShowAlert, setShouldShowAlert,
editorContentCustomClassNames, editorContentCustomClassNames,
value, value,
initialValue,
uploadFile, uploadFile,
deleteFile, deleteFile,
noBorder, noBorder,
@ -97,6 +99,10 @@ const RichTextEditor = ({
customClassName, customClassName,
}); });
React.useEffect(() => {
if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
}, [editor, initialValue]);
if (!editor) return null; if (!editor) return null;
return ( return (

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

@ -1,8 +1,7 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
// hooks // hooks
import { import {
useApplication, useApplication,
@ -11,7 +10,6 @@ import {
useProject, useProject,
useProjectState, useProjectState,
useUser, useUser,
useInbox,
useMember, useMember,
} from "hooks/store"; } from "hooks/store";
// components // components
@ -54,7 +52,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { getInboxesByProjectId, getInboxById } = useInbox();
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
@ -101,9 +98,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
[workspaceSlug, projectId, updateFilters] [workspaceSlug, projectId, updateFilters]
); );
const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined;
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
const canUserCreateIssue = const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
@ -154,7 +148,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />} link={
<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
}
/> />
</Breadcrumbs> </Breadcrumbs>
</div> </div>
@ -201,24 +197,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
/> />
</FiltersDropdown> </FiltersDropdown>
</div> </div>
{currentProjectDetails?.inbox_view && inboxDetails && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
<span className="hidden md:block" >
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails?.pending_issue_count > 0 && (
<span className="absolute -right-1.5 -top-1.5 h-4 w-4 rounded-full border border-custom-sidebar-border-200 bg-custom-sidebar-background-80 text-custom-text-100">
{inboxDetails?.pending_issue_count}
</span>
)}
</Button>
</span>
<Inbox className="w-4 h-4 mr-2 text-custom-text-200 block md:hidden" />
</Link>
)}
{canUserCreateIssue && ( {canUserCreateIssue && (
<> <>
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
Analytics Analytics
</Button> </Button>
<Button <Button

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
// hooks // hooks
import { useInboxIssues } from "hooks/store"; import { useInboxIssues } from "hooks/store";
// ui // ui
import { Loader } from "@plane/ui"; import { InboxSidebarLoader } from "components/ui";
// components // components
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../"; import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
@ -21,6 +21,10 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
issues: { loader }, issues: { loader },
} = useInboxIssues(); } = useInboxIssues();
if (loader === "init-loader") {
return <InboxSidebarLoader />;
}
return ( return (
<div className="relative flex flex-col w-full h-full"> <div className="relative flex flex-col w-full h-full">
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300"> <div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
@ -28,7 +32,6 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80"> <div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
<Inbox className="w-4 h-4" /> <Inbox className="w-4 h-4" />
</div> </div>
<div className="font-medium">Inbox</div>
</div> </div>
<div className="z-20"> <div className="z-20">
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
@ -39,18 +42,9 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
</div> </div>
{loader && ["init-loader", "mutation"].includes(loader) ? (
<Loader className="flex flex-col h-full gap-5 p-5">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
) : (
<div className="w-full h-full overflow-hidden"> <div className="w-full h-full overflow-hidden">
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
</div> </div>
)}
</div> </div>
); );
}); });

View File

@ -1,5 +1,4 @@
import { FC, useState, useEffect } from "react"; import { FC, useState, useEffect } from "react";
import { observer } from "mobx-react";
// components // components
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor";
@ -14,24 +13,20 @@ import { TIssueOperations } from "./issue-detail";
import useDebounce from "hooks/use-debounce"; import useDebounce from "hooks/use-debounce";
export type IssueDescriptionInputProps = { export type IssueDescriptionInputProps = {
disabled?: boolean;
value: string | undefined | null;
workspaceSlug: string; workspaceSlug: string;
isSubmitting: "submitting" | "submitted" | "saved";
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
issueOperations: TIssueOperations;
projectId: string; projectId: string;
issueId: string; issueId: string;
value: string | undefined;
initialValue: string | undefined;
disabled?: boolean;
issueOperations: TIssueOperations;
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
}; };
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => { export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = (props) => {
const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; const { workspaceSlug, projectId, issueId, value, initialValue, disabled, issueOperations, setIsSubmitting } = props;
// states // states
const [descriptionHTML, setDescriptionHTML] = useState(value); const [descriptionHTML, setDescriptionHTML] = useState(value);
const [localIssueDescription, setLocalIssueDescription] = useState({
id: issueId,
description_html: typeof value === "string" && value != "" ? value : "<p></p>",
});
// store hooks // store hooks
const { mentionHighlights, mentionSuggestions } = useMention(); const { mentionHighlights, mentionSuggestions } = useMention();
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
@ -41,32 +36,22 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
useEffect(() => { useEffect(() => {
if (value) setDescriptionHTML(value); setDescriptionHTML(value);
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
if (issueId && value) if (debouncedValue && debouncedValue !== value) {
setLocalIssueDescription({
id: issueId,
description_html: typeof value === "string" && value != "" ? value : "<p></p>",
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [issueId, value]);
useEffect(() => {
if (debouncedValue || debouncedValue === "") {
setIsSubmitting("submitted");
issueOperations issueOperations
.update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false)
.finally(() => { .finally(() => {
setIsSubmitting("saved"); setIsSubmitting("submitted");
}); });
} }
// DO NOT Add more dependencies here. It will cause multiple requests to be sent. // DO NOT Add more dependencies here. It will cause multiple requests to be sent.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue]); }, [debouncedValue]);
if (!descriptionHTML && descriptionHTML !== "") { if (!descriptionHTML) {
return ( return (
<Loader> <Loader>
<Loader.Item height="150px" /> <Loader.Item height="150px" />
@ -92,18 +77,15 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
deleteFile={fileService.getDeleteImageFunction(workspaceId)} deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)}
value={descriptionHTML} value={descriptionHTML}
rerenderOnPropsChange={localIssueDescription} initialValue={initialValue}
// setShouldShowAlert={setShowAlert}
// setIsSubmitting={setIsSubmitting}
dragDropEnabled dragDropEnabled
customClassName="min-h-[150px] shadow-sm" customClassName="min-h-[150px] shadow-sm"
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
// setShowAlert(true);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
setDescriptionHTML(description_html); setDescriptionHTML(description_html === "" ? "<p></p>" : description_html);
}} }}
mentionSuggestions={mentionSuggestions} mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights} mentionHighlights={mentionHighlights}
/> />
); );
}); };

View File

@ -1,7 +1,8 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import { useIssueDetail, useProjectState, useUser } from "hooks/store";
import useReloadConfirmations from "hooks/use-reload-confirmation";
// components // components
import { IssueUpdateStatus, TIssueOperations } from "components/issues"; import { IssueUpdateStatus, TIssueOperations } from "components/issues";
import { IssueTitleInput } from "../../title-input"; import { IssueTitleInput } from "../../title-input";
@ -31,12 +32,31 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
const issue = getIssueById(issueId); useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 3000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert, setIsSubmitting]);
const issue = issueId ? getIssueById(issueId) : undefined;
if (!issue) return <></>; if (!issue) return <></>;
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
const issueDescription =
issue.description_html !== undefined || issue.description_html !== null
? issue.description_html != ""
? issue.description_html
: "<p></p>"
: undefined;
return ( return (
<> <>
<div className="rounded-lg space-y-4"> <div className="rounded-lg space-y-4">
@ -74,11 +94,11 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={issue.project_id} projectId={issue.project_id}
issueId={issue.id} issueId={issue.id}
isSubmitting={isSubmitting} value={issueDescription}
setIsSubmitting={(value) => setIsSubmitting(value)} initialValue={issueDescription}
issueOperations={issueOperations}
disabled={!is_editable} disabled={!is_editable}
value={issue.description_html} issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
{currentUser && ( {currentUser && (

View File

@ -1,7 +1,8 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import { useIssueDetail, useProjectState, useUser } from "hooks/store";
import useReloadConfirmations from "hooks/use-reload-confirmation";
// components // components
import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
import { IssueTitleInput } from "../title-input"; import { IssueTitleInput } from "../title-input";
@ -33,12 +34,31 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
const issue = getIssueById(issueId); useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert, setIsSubmitting]);
const issue = issueId ? getIssueById(issueId) : undefined;
if (!issue) return <></>; if (!issue) return <></>;
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
const issueDescription =
issue.description_html !== undefined || issue.description_html !== null
? issue.description_html != ""
? issue.description_html
: "<p></p>"
: undefined;
return ( return (
<> <>
<div className="rounded-lg space-y-4"> <div className="rounded-lg space-y-4">
@ -78,11 +98,11 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={issue.project_id} projectId={issue.project_id}
issueId={issue.id} issueId={issue.id}
isSubmitting={isSubmitting} value={issueDescription}
setIsSubmitting={(value) => setIsSubmitting(value)} initialValue={issueDescription}
issueOperations={issueOperations}
disabled={!is_editable} disabled={!is_editable}
value={issue.description_html} issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
{currentUser && ( {currentUser && (

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

@ -30,12 +30,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
} = useIssueDetail(); } = useIssueDetail();
// hooks // hooks
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
// derived values
const issue = getIssueById(issueId);
if (!issue) return <></>;
const projectDetails = getProjectById(issue?.project_id);
useEffect(() => { useEffect(() => {
if (isSubmitting === "submitted") { if (isSubmitting === "submitted") {
@ -48,6 +42,18 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
} }
}, [isSubmitting, setShowAlert, setIsSubmitting]); }, [isSubmitting, setShowAlert, setIsSubmitting]);
const issue = issueId ? getIssueById(issueId) : undefined;
if (!issue) return <></>;
const projectDetails = getProjectById(issue?.project_id);
const issueDescription =
issue.description_html !== undefined || issue.description_html !== null
? issue.description_html != ""
? issue.description_html
: "<p></p>"
: undefined;
return ( return (
<> <>
<span className="text-base font-medium text-custom-text-400"> <span className="text-base font-medium text-custom-text-400">
@ -63,16 +69,18 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
disabled={disabled} disabled={disabled}
value={issue.name} value={issue.name}
/> />
<IssueDescriptionInput <IssueDescriptionInput
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={issue.project_id} projectId={issue.project_id}
issueId={issue.id} issueId={issue.id}
isSubmitting={isSubmitting} value={issueDescription}
setIsSubmitting={(value) => setIsSubmitting(value)} initialValue={issueDescription}
issueOperations={issueOperations}
disabled={disabled} disabled={disabled}
value={issue.description_html} issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
{currentUser && ( {currentUser && (
<IssueReaction <IssueReaction
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}

View File

@ -31,7 +31,7 @@ export const IssueTitleInput: FC<IssueTitleInputProps> = observer((props) => {
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
if (debouncedValue) { if (debouncedValue && debouncedValue !== value) {
issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => {
setIsSubmitting("saved"); setIsSubmitting("saved");
}); });

View File

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

View File

@ -1,7 +1,7 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// hooks // hooks
import { useEventTracker, useProject, useWorkspace } from "hooks/store"; import { useEventTracker, useProject } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import EmojiIconPicker from "components/emoji-icon-picker"; import EmojiIconPicker from "components/emoji-icon-picker";
@ -19,22 +19,19 @@ import { NETWORK_CHOICES } from "constants/project";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { PROJECT_UPDATED } from "constants/event-tracker"; import { PROJECT_UPDATED } from "constants/event-tracker";
export interface IProjectDetailsForm { export interface IProjectDetailsForm {
project: IProject; project: IProject;
workspaceSlug: string; workspaceSlug: string;
projectId: string;
isAdmin: boolean; isAdmin: boolean;
} }
const projectService = new ProjectService(); const projectService = new ProjectService();
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => { export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const { project, workspaceSlug, isAdmin } = props; const { project, workspaceSlug, projectId, isAdmin } = props;
// states // states
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// store hooks // store hooks
const { captureProjectEvent } = useEventTracker(); const { captureProjectEvent } = useEventTracker();
const { currentWorkspace } = useWorkspace();
const { updateProject } = useProject(); const { updateProject } = useProject();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -47,6 +44,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setError, setError,
reset, reset,
formState: { errors, dirtyFields }, formState: { errors, dirtyFields },
getValues,
} = useForm<IProject>({ } = useForm<IProject>({
defaultValues: { defaultValues: {
...project, ...project,
@ -56,26 +54,23 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
}); });
useEffect(() => { useEffect(() => {
if (!project) return; if (project && projectId !== getValues("id")) {
reset({ reset({
...project, ...project,
emoji_and_icon: project.emoji ?? project.icon_prop, emoji_and_icon: project.emoji ?? project.icon_prop,
workspace: (project.workspace as IWorkspace).id, workspace: (project.workspace as IWorkspace).id,
}); });
}, [project, reset]); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project, projectId]);
const handleIdentifierChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleIdentifierChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target; const { value } = event.target;
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, ""); const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
const formattedValue = alphanumericValue.toUpperCase(); const formattedValue = alphanumericValue.toUpperCase();
setValue("identifier", formattedValue); setValue("identifier", formattedValue);
}; };
const handleUpdateChange = async (payload: Partial<IProject>) => { const handleUpdateChange = async (payload: Partial<IProject>) => {
if (!workspaceSlug || !project) return; if (!workspaceSlug || !project) return;
return updateProject(workspaceSlug.toString(), project.id, payload) return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => { .then((res) => {
const changed_properties = Object.keys(dirtyFields); const changed_properties = Object.keys(dirtyFields);
@ -107,11 +102,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
}); });
}); });
}; };
const onSubmit = async (formData: IProject) => { const onSubmit = async (formData: IProject) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
setIsLoading(true); setIsLoading(true);
const payload: Partial<IProject> = { const payload: Partial<IProject> = {
name: formData.name, name: formData.name,
network: formData.network, network: formData.network,
@ -119,7 +112,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
description: formData.description, description: formData.description,
cover_image: formData.cover_image, cover_image: formData.cover_image,
}; };
if (typeof formData.emoji_and_icon === "object") { if (typeof formData.emoji_and_icon === "object") {
payload.emoji = null; payload.emoji = null;
payload.icon_prop = formData.emoji_and_icon; payload.icon_prop = formData.emoji_and_icon;
@ -127,7 +119,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
payload.emoji = formData.emoji_and_icon; payload.emoji = formData.emoji_and_icon;
payload.icon_prop = null; payload.icon_prop = null;
} }
if (project.identifier !== formData.identifier) if (project.identifier !== formData.identifier)
await projectService await projectService
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
@ -136,20 +127,16 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
else await handleUpdateChange(payload); else await handleUpdateChange(payload);
}); });
else await handleUpdateChange(payload); else await handleUpdateChange(payload);
setTimeout(() => { setTimeout(() => {
setIsLoading(false); setIsLoading(false);
}, 300); }, 300);
}; };
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network); const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="relative mt-6 h-44 w-full"> <div className="relative mt-6 h-44 w-full">
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" /> <img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4"> <div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate"> <div className="flex flex-grow gap-3 truncate">
@ -180,7 +167,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
</span> </span>
</div> </div>
</div> </div>
<div className="flex flex-shrink-0 justify-center"> <div className="flex flex-shrink-0 justify-center">
<div> <div>
<Controller <Controller
@ -225,7 +211,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm">Description</h4> <h4 className="text-sm">Description</h4>
<Controller <Controller
@ -245,7 +230,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
)} )}
/> />
</div> </div>
<div className="flex w-full items-center justify-between gap-10"> <div className="flex w-full items-center justify-between gap-10">
<div className="flex w-1/2 flex-col gap-1"> <div className="flex w-1/2 flex-col gap-1">
<h4 className="text-sm">Identifier</h4> <h4 className="text-sm">Identifier</h4>
@ -280,7 +264,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
)} )}
/> />
</div> </div>
<div className="flex w-1/2 flex-col gap-1"> <div className="flex w-1/2 flex-col gap-1">
<h4 className="text-sm">Network</h4> <h4 className="text-sm">Network</h4>
<Controller <Controller
@ -306,7 +289,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<> <>
<Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}> <Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}>

View File

@ -16,12 +16,15 @@ import {
LogOut, LogOut,
ChevronDown, ChevronDown,
MoreHorizontal, MoreHorizontal,
Inbox,
} from "lucide-react"; } from "lucide-react";
// hooks // hooks
import { useApplication, useEventTracker, useProject } from "hooks/store"; import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// helpers // helpers
import { cn } from "helpers/common.helper";
import { getNumberCount } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// components // components
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui"; import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
@ -62,6 +65,11 @@ const navigation = (workspaceSlug: string, projectId: string) => [
href: `/${workspaceSlug}/projects/${projectId}/pages`, href: `/${workspaceSlug}/projects/${projectId}/pages`,
Icon: FileText, Icon: FileText,
}, },
{
name: "Inbox",
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
Icon: Inbox,
},
{ {
name: "Settings", name: "Settings",
href: `/${workspaceSlug}/projects/${projectId}/settings`, href: `/${workspaceSlug}/projects/${projectId}/settings`,
@ -75,7 +83,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { currentProjectDetails, addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
const { getInboxesByProjectId, getInboxById } = useInbox();
// states // states
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false);
@ -96,6 +105,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const actionSectionRef = useRef<HTMLDivElement | null>(null); const actionSectionRef = useRef<HTMLDivElement | null>(null);
const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined;
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
const handleAddToFavorites = () => { const handleAddToFavorites = () => {
if (!workspaceSlug || !project) return; if (!workspaceSlug || !project) return;
@ -147,7 +159,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
{({ open }) => ( {({ open }) => (
<> <>
<div <div
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${snapshot?.isDragging ? "opacity-60" : "" className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${
snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`} } ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
> >
{provided && ( {provided && (
@ -157,8 +170,10 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
> >
<button <button
type="button" type="button"
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${isCollapsed ? "" : "group-hover:!flex" className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : "" isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
isMenuActive ? "!flex" : ""
}`} }`}
{...provided?.dragHandleProps} {...provided?.dragHandleProps}
> >
@ -170,11 +185,13 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}> <Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
<Disclosure.Button <Disclosure.Button
as="div" as="div"
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${isCollapsed ? "justify-center" : `justify-between` className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
isCollapsed ? "justify-center" : `justify-between`
}`} }`}
> >
<div <div
className={`flex w-full flex-grow items-center gap-x-2 truncate ${isCollapsed ? "justify-center" : "" className={`flex w-full flex-grow items-center gap-x-2 truncate ${
isCollapsed ? "justify-center" : ""
}`} }`}
> >
{project.emoji ? ( {project.emoji ? (
@ -195,7 +212,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<ChevronDown <ChevronDown
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : "" className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`} } mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
/> />
)} )}
@ -306,7 +324,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
(item.name === "Cycles" && !project.cycle_view) || (item.name === "Cycles" && !project.cycle_view) ||
(item.name === "Modules" && !project.module_view) || (item.name === "Modules" && !project.module_view) ||
(item.name === "Views" && !project.issue_views_view) || (item.name === "Views" && !project.issue_views_view) ||
(item.name === "Pages" && !project.page_view) (item.name === "Pages" && !project.page_view) ||
(item.name === "Inbox" && !project.inbox_view)
) )
return; return;
@ -320,13 +339,42 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
disabled={!isCollapsed} disabled={!isCollapsed}
> >
<div <div
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${router.asPath.includes(item.href) className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100" ? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80" : "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`} } ${isCollapsed ? "justify-center" : ""}`}
> >
{item.name === "Inbox" && inboxDetails ? (
<>
<div className="flex items-center justify-center relative">
{inboxDetails?.pending_issue_count > 0 && (
<span
className={cn(
"absolute -right-1.5 -top-1 px-0.5 h-3.5 w-3.5 flex items-center tracking-tight justify-center rounded-full text-[0.5rem] border-[0.5px] border-custom-sidebar-border-200 bg-custom-background-80 text-custom-text-100",
{
"text-[0.375rem] leading-5": inboxDetails?.pending_issue_count >= 100,
},
{
"border-none bg-custom-primary-300 text-white": router.asPath.includes(
item.href
),
}
)}
>
{getNumberCount(inboxDetails?.pending_issue_count)}
</span>
)}
<item.Icon className="h-4 w-4 stroke-[1.5]" />
</div>
{!isCollapsed && item.name}
</>
) : (
<>
<item.Icon className="h-4 w-4 stroke-[1.5]" /> <item.Icon className="h-4 w-4 stroke-[1.5]" />
{!isCollapsed && item.name} {!isCollapsed && item.name}
</>
)}
</div> </div>
</Tooltip> </Tooltip>
</span> </span>

View File

@ -3,3 +3,4 @@ export * from "./kanban-layout-loader";
export * from "./calendar-layout-loader"; export * from "./calendar-layout-loader";
export * from "./spreadsheet-layout-loader"; export * from "./spreadsheet-layout-loader";
export * from "./gantt-layout-loader"; export * from "./gantt-layout-loader";
export * from "./project-inbox";

View File

@ -0,0 +1,19 @@
import React from "react";
// ui
import { InboxSidebarLoader } from "./inbox-sidebar-loader";
export const InboxLayoutLoader = () => (
<div className="relative flex h-full overflow-hidden">
<InboxSidebarLoader />
<div className="w-full">
<div className="grid h-full place-items-center p-4 text-custom-text-200">
<div className="grid h-full place-items-center">
<div className="my-5 flex flex-col items-center gap-4">
<span className="h-[60px] w-[60px] bg-custom-background-80 rounded" />
<span className="h-6 w-96 bg-custom-background-80 rounded" />
</div>
</div>
</div>
</div>
</div>
);

View File

@ -0,0 +1,24 @@
import React from "react";
export const InboxSidebarLoader = () => (
<div className="h-full w-[340px] border-r border-custom-border-300">
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
<span className="h-6 w-16 bg-custom-background-80 rounded" />
<span className="h-6 w-16 bg-custom-background-80 rounded" />
</div>
<div className="flex flex-col">
{[...Array(6)].map(() => (
<div className="flex flex-col gap-3 h-[5rem]space-y-3 border-b border-custom-border-200 px-4 py-2">
<div className="flex items-center justify-between gap-3">
<span className="h-5 w-20 bg-custom-background-80 rounded" />
<span className="h-5 w-16 bg-custom-background-80 rounded" />
</div>
<div className="flex items-center gap-3">
<span className="h-5 w-5 bg-custom-background-80 rounded" />
<span className="h-5 w-16 bg-custom-background-80 rounded" />
</div>
</div>
))}
</div>
</div>
);

View File

@ -0,0 +1,2 @@
export * from "./inbox-layout-loader";
export * from "./inbox-sidebar-loader";

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

@ -41,13 +41,11 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props)
} }
/> />
) : ( ) : (
<div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full gap-2 overflow-x-hidden overflow-y-scroll"> <div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block"> <div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar /> <ProjectSettingsSidebar />
</div> </div>
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0"> <div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0 overflow-x-hidden overflow-y-scroll">{children}</div>
{children}
</div>
</div> </div>
); );
}); });

View File

@ -1,6 +1,8 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// ui
import { Loader } from "@plane/ui";
// hooks // hooks
import { useUser } from "hooks/store"; import { useUser } from "hooks/store";
// constants // constants
@ -16,6 +18,21 @@ export const ProjectSettingsSidebar = () => {
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST; const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
if (!currentProjectRole) {
return (
<div className="flex w-80 flex-col gap-6 px-5">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<Loader className="flex w-full flex-col gap-2">
{[...Array(8)].map(() => (
<Loader.Item height="34px" />
))}
</Loader>
</div>
</div>
);
}
return ( return (
<div className="flex w-80 flex-col gap-6 px-5"> <div className="flex w-80 flex-col gap-6 px-5">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import { observer } from "mobx-react";
import { useInbox, useProject } from "hooks/store"; import { useInbox, useProject } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// ui
import { InboxLayoutLoader } from "components/ui";
// components // components
import { ProjectInboxHeader } from "components/headers"; import { ProjectInboxHeader } from "components/headers";
// types // types
@ -33,7 +35,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{currentProjectDetails?.inbox_view ? <div>Loading...</div> : <div>You don{"'"}t have access to inbox</div>} {currentProjectDetails?.inbox_view ? <InboxLayoutLoader /> : <div>You don{"'"}t have access to inbox</div>}
</div> </div>
); );
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import { useProject } from "hooks/store";
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
// components // components
import { PageHead } from "components/core";
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
import { import {
DeleteProjectModal, DeleteProjectModal,
@ -28,18 +29,19 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
const { currentProjectDetails, fetchProjectDetails } = useProject(); const { currentProjectDetails, fetchProjectDetails } = useProject();
// api call to fetch project details // api call to fetch project details
// TODO: removed this API if not necessary // TODO: removed this API if not necessary
useSWR( const { isLoading } = useSWR(
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}
@ -49,10 +51,11 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
)} )}
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}> <div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
{currentProjectDetails && workspaceSlug ? ( {currentProjectDetails && workspaceSlug && projectId && !isLoading ? (
<ProjectDetailsForm <ProjectDetailsForm
project={currentProjectDetails} project={currentProjectDetails}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isAdmin={isAdmin} isAdmin={isAdmin}
/> />
) : ( ) : (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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