mirror of
https://github.com/makeplane/plane.git
synced 2026-04-28 03:49:45 -05:00
[WEB-3759] chore: header revamp for cycles, modules, pages and views (#6875)
* chore: header revamp for cycles, modules, pages and views * chore: moved list fetch to layout level
This commit is contained in:
Vendored
+8
@@ -26,3 +26,11 @@ export type TLogoProps = {
|
||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||
|
||||
export type TFetchStatus = "partial" | "complete" | undefined;
|
||||
|
||||
export type ICustomSearchSelectOption = {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FIXME: fix this!!!
|
||||
import { Placement } from "@blueprintjs/popover2";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
|
||||
export interface IDropdownProps {
|
||||
customButtonClassName?: string;
|
||||
@@ -44,15 +45,7 @@ interface CustomSearchSelectProps {
|
||||
onChange: any;
|
||||
onClose?: () => void;
|
||||
noResultsMessage?: string;
|
||||
options:
|
||||
| {
|
||||
value: any;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
options?: ICustomSearchSelectOption[];
|
||||
}
|
||||
|
||||
interface SingleValueProps {
|
||||
|
||||
+61
-38
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
@@ -18,12 +18,18 @@ import {
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { CycleQuickActions } from "@/components/cycles";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -69,6 +75,8 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
||||
};
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// router
|
||||
@@ -159,6 +167,25 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const switcherOptions = currentProjectCycleIds
|
||||
?.map((id) => {
|
||||
const _cycle = id === cycleId ? cycleDetails : getCycleById(id);
|
||||
if (!_cycle) return;
|
||||
const cycleLink = `/${workspaceSlug}/projects/${projectId}/cycles/${_cycle.id}`;
|
||||
return {
|
||||
value: _cycle.id,
|
||||
query: _cycle.name,
|
||||
content: (
|
||||
<Link href={cycleLink}>
|
||||
<SwitcherLabel name={_cycle.name} LabelIcon={ContrastIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
@@ -201,33 +228,29 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={cycleId}
|
||||
onChange={() => {}}
|
||||
label={
|
||||
<>
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${issuesCount} ${
|
||||
issuesCount > 1 ? "work items" : "work item"
|
||||
} in this cycle`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{issuesCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={cycleDetails?.name} LabelIcon={ContrastIcon} />
|
||||
{workItemsCount && workItemsCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${workItemsCount} ${
|
||||
workItemsCount > 1 ? "work items" : "work item"
|
||||
} in this cycle`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{workItemsCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
className="ml-1.5 flex-shrink-0 truncate"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)}
|
||||
</CustomMenu>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
@@ -302,19 +325,19 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
<CycleQuickActions
|
||||
parentRef={parentRef}
|
||||
cycleId={cycleId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center size-6 bg-custom-background-80/70 rounded"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 md:hidden"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
|
||||
+47
-29
@@ -16,12 +16,17 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -155,7 +160,24 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const switcherOptions = projectModuleIds
|
||||
?.map((id) => {
|
||||
const _module = id === moduleId ? moduleDetails : getModuleById(id);
|
||||
if (!_module) return;
|
||||
const moduleLink = `/${workspaceSlug}/projects/${projectId}/modules/${_module.id}`;
|
||||
return {
|
||||
value: _module.id,
|
||||
query: _module.name,
|
||||
content: (
|
||||
<Link href={moduleLink}>
|
||||
<SwitcherLabel name={_module.name} LabelIcon={DiceIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -196,33 +218,29 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
label={
|
||||
<>
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${issuesCount} ${
|
||||
issuesCount > 1 ? "work items" : "work item"
|
||||
} in this module`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{issuesCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={moduleDetails?.name} LabelIcon={DiceIcon} />
|
||||
{workItemsCount && workItemsCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`There are ${workItemsCount} ${
|
||||
workItemsCount > 1 ? "work items" : "work item"
|
||||
} in this module`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||
{workItemsCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
className="ml-1.5 flex-shrink-0"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
||||
</CustomMenu>
|
||||
value={moduleId}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
+35
-91
@@ -1,27 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
import { FileText, Layers } from "lucide-react";
|
||||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { PageEditInformationPopover } from "@/components/pages";
|
||||
// helpers
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage } from "@/plane-web/hooks/store";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
||||
export interface IPagesHeaderProps {
|
||||
showButton?: boolean;
|
||||
@@ -29,42 +25,36 @@ export interface IPagesHeaderProps {
|
||||
|
||||
export const PageDetailsHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, pageId } = useParams();
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { workspaceSlug, pageId, projectId } = useParams();
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const page = usePage({
|
||||
pageId: pageId?.toString() ?? "",
|
||||
storeType: EPageStoreType.PROJECT,
|
||||
});
|
||||
if (!page) return null;
|
||||
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
|
||||
// derived values
|
||||
const { name, logo_props, updatePageLogo, isContentEditable } = page;
|
||||
// use platform
|
||||
const { isMobile } = usePlatformOS();
|
||||
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
||||
|
||||
const handlePageLogoUpdate = async (data: TLogoProps) => {
|
||||
if (data) {
|
||||
updatePageLogo(data)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Logo Updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!page) return null;
|
||||
const switcherOptions = projectPageIds
|
||||
.map((id) => {
|
||||
const _page = id === pageId ? page : getPageById(id);
|
||||
if (!_page) return;
|
||||
const pageLink = `/${workspaceSlug}/projects/${projectId}/pages/${_page.id}`;
|
||||
return {
|
||||
value: _page.id,
|
||||
query: _page.name,
|
||||
content: (
|
||||
<Link href={pageLink} className="flex gap-2 items-center justify-between">
|
||||
<SwitcherLabel logo_props={_page.logo_props} name={_page.name} LabelIcon={Layers} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
const pageTitle = getPageName(name);
|
||||
if (!page) return null;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
@@ -99,60 +89,14 @@ export const PageDetailsHeader = observer(() => {
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden">
|
||||
<EmojiIconPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={(val: boolean) => setIsOpen(val)}
|
||||
className="flex items-center justify-center"
|
||||
buttonClassName="flex items-center justify-center"
|
||||
label={
|
||||
<>
|
||||
{logo_props?.in_use ? (
|
||||
<Logo logo={logo_props} size={16} type="lucide" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onChange={(val) => {
|
||||
let logoValue = {};
|
||||
|
||||
if (val?.type === "emoji")
|
||||
logoValue = {
|
||||
value: convertHexEmojiToDecimal(val.value.unified),
|
||||
url: val.value.imageUrl,
|
||||
};
|
||||
else if (val?.type === "icon") logoValue = val.value;
|
||||
|
||||
handlePageLogoUpdate({
|
||||
in_use: val?.type,
|
||||
[val?.type]: logoValue,
|
||||
}).finally(() => setIsOpen(false));
|
||||
}}
|
||||
defaultIconColor={
|
||||
logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined
|
||||
}
|
||||
defaultOpen={
|
||||
logo_props?.in_use && logo_props?.in_use === "emoji"
|
||||
? EmojiIconPickerTypes.EMOJI
|
||||
: EmojiIconPickerTypes.ICON
|
||||
}
|
||||
disabled={!isContentEditable}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip tooltipContent={pageTitle} position="bottom" isMobile={isMobile}>
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">
|
||||
{pageTitle}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
type="component"
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
value={pageId}
|
||||
options={switcherOptions}
|
||||
label={<SwitcherLabel logo_props={page.logo_props} name={page.name} LabelIcon={Layers} />}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
+11
@@ -1,11 +1,22 @@
|
||||
"use client";
|
||||
|
||||
// component
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
// local components
|
||||
import { PageDetailsHeader } from "./header";
|
||||
|
||||
export default function ProjectPageDetailsLayout({ children }: { children: React.ReactNode }) {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { fetchPagesList } = usePageStore(EPageStoreType.PROJECT);
|
||||
// fetching pages list
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_PAGES_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchPagesList(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<PageDetailsHeader />} />
|
||||
|
||||
+41
-50
@@ -16,17 +16,21 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useCommandPalette,
|
||||
@@ -143,6 +147,23 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
if (!viewDetails) return;
|
||||
|
||||
const switcherOptions = projectViewIds
|
||||
?.map((id) => {
|
||||
const _view = id === viewId ? viewDetails : getViewById(id);
|
||||
if (!_view) return;
|
||||
const viewLink = `/${workspaceSlug}/projects/${projectId}/views/${_view.id}`;
|
||||
return {
|
||||
value: _view.id,
|
||||
query: _view.name,
|
||||
content: (
|
||||
<Link href={viewLink}>
|
||||
<SwitcherLabel logo_props={_view.logo_props} name={_view.name} LabelIcon={Layers} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
@@ -161,42 +182,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
{viewDetails?.logo_props?.in_use ? (
|
||||
<Logo logo={viewDetails.logo_props} size={12} type="lucide" />
|
||||
) : (
|
||||
<Layers height={12} width={12} />
|
||||
)}
|
||||
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
className="ml-1.5"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{projectViewIds?.map((viewId) => {
|
||||
const view = getViewById(viewId);
|
||||
|
||||
if (!view) return;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={viewId}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/views/${viewId}`}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{view?.logo_props?.in_use ? (
|
||||
<Logo logo={view.logo_props} size={12} type="lucide" />
|
||||
) : (
|
||||
<Layers height={12} width={12} />
|
||||
)}
|
||||
{truncateText(view.name, 40)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={viewId}
|
||||
label={<SwitcherLabel logo_props={viewDetails.logo_props} name={viewDetails.name} LabelIcon={Layers} />}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
@@ -210,17 +201,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div className="hidden md:block">
|
||||
<ViewQuickActions
|
||||
parentRef={parentRef}
|
||||
projectId={projectId.toString()}
|
||||
view={viewDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<Header.RightItem className="items-center">
|
||||
{!viewDetails?.is_locked ? (
|
||||
<>
|
||||
<LayoutSelection
|
||||
@@ -287,6 +269,15 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="hidden md:block">
|
||||
<ViewQuickActions
|
||||
parentRef={parentRef}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center size-6 bg-custom-background-80/70 rounded"
|
||||
projectId={projectId.toString()}
|
||||
view={viewDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</div>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from "./logo";
|
||||
export * from "./pro-icon";
|
||||
export * from "./count-chip";
|
||||
export * from "./activity";
|
||||
export * from "./switcher-label";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FC } from "react";
|
||||
import { TLogoProps } from "@plane/types";
|
||||
import { ISvgIcons, Logo } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
type TSwitcherLabelProps = {
|
||||
logo_props?: TLogoProps;
|
||||
logo_url?: string;
|
||||
name?: string;
|
||||
LabelIcon: FC<ISvgIcons>;
|
||||
};
|
||||
|
||||
export const SwitcherLabel: FC<TSwitcherLabelProps> = (props) => {
|
||||
const { logo_props, name, LabelIcon, logo_url } = props;
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
{logo_props?.in_use ? (
|
||||
<Logo logo={logo_props} size={12} type="lucide" />
|
||||
) : logo_url ? (
|
||||
<img src={getFileURL(logo_url)} alt="logo" className="rounded-sm w-3 h-3 object-cover" />
|
||||
) : (
|
||||
<LabelIcon height={12} width={12} />
|
||||
)}
|
||||
{truncateText(name ?? "", 40)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,44 +75,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
|
||||
const handleRestoreCycle = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await restoreCycle(workspaceSlug.toString(), projectId.toString(), cycleDetails.id)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("project_cycles.action.restore.success.title"),
|
||||
message: t("project_cycles.action.restore.success.description"),
|
||||
});
|
||||
router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/archives/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("project_cycles.action.restore.failed.title"),
|
||||
message: t("project_cycles.action.restore.failed.description"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
message: t("common.link_copied_to_clipboard"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("common.errors.default.message"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
|
||||
|
||||
@@ -224,62 +186,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{!isArchived && (
|
||||
<button onClick={handleCopyText} className="size-4">
|
||||
<LinkIcon className="size-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu
|
||||
placement="bottom-end"
|
||||
customButtonClassName="size-4"
|
||||
customButton={<EllipsisIcon className="size-3.5 text-custom-text-300" />}
|
||||
>
|
||||
{!isArchived && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveCycleModal(true)} disabled={!isCompleted}>
|
||||
{isCompleted ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
{t("common.archive")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>{t("common.archive")}</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
{t("project_cycles.only_completed_cycles_can_be_archived")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchived && (
|
||||
<CustomMenu.MenuItem onClick={handleRestoreCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<ArchiveRestoreIcon className="h-3 w-3" />
|
||||
<span>{t("project_cycles.action.restore.title")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_SIDEBAR");
|
||||
setCycleDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>{t("delete")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-start justify-between gap-3 pt-2">
|
||||
|
||||
@@ -24,10 +24,11 @@ type Props = {
|
||||
cycleId: string;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, cycleId, projectId, workspaceSlug } = props;
|
||||
const { parentRef, cycleId, projectId, workspaceSlug, customClassName } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// states
|
||||
@@ -188,7 +189,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect maxHeight="lg">
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect maxHeight="lg" buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -4,18 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
ArchiveRestoreIcon,
|
||||
CalendarClock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Info,
|
||||
LinkIcon,
|
||||
Plus,
|
||||
SquareUser,
|
||||
Trash2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane types
|
||||
import {
|
||||
@@ -30,18 +19,7 @@ import {
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
||||
// plane ui
|
||||
import {
|
||||
CustomMenu,
|
||||
Loader,
|
||||
LayersIcon,
|
||||
CustomSelect,
|
||||
ModuleStatusIcon,
|
||||
TOAST_TYPE,
|
||||
setToast,
|
||||
ArchiveIcon,
|
||||
TextArea,
|
||||
} from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
|
||||
import {
|
||||
@@ -55,7 +33,6 @@ import {
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
|
||||
@@ -82,23 +59,18 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
const [moduleLinkModal, setModuleLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } =
|
||||
useModule();
|
||||
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
|
||||
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
||||
const { captureModuleEvent, captureEvent } = useEventTracker();
|
||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
const moduleState = moduleDetails?.status?.toLocaleLowerCase();
|
||||
const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
|
||||
|
||||
const areEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId.toString());
|
||||
const estimateType = areEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
||||
const isEstimatePointValid = estimateType && estimateType?.type == EEstimateSystem.POINTS ? true : false;
|
||||
@@ -175,24 +147,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Module link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Some error occurred",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => {
|
||||
submitChanges({
|
||||
start_date: startDate ? renderFormattedPayloadDate(startDate) : null,
|
||||
@@ -205,30 +159,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestoreModule = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
await restoreModule(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your module can be found in project modules.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Module could not be restored. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (moduleDetails)
|
||||
reset({
|
||||
@@ -309,56 +239,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3.5">
|
||||
{!isArchived && (
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu placement="bottom-end" ellipsis>
|
||||
{!isArchived && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveModuleModal(true)} disabled={!isInArchivableGroup}>
|
||||
{isInArchivableGroup ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
{t("project_module.archive_module")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive module</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
{t("project_module.quick_actions.archive_module_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchived && (
|
||||
<CustomMenu.MenuItem onClick={handleRestoreModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<ArchiveRestoreIcon className="h-3 w-3" />
|
||||
<span>{t("project_module.restore_module")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Module peek-overview");
|
||||
setModuleDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>{t("project_module.delete_module")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -24,10 +24,11 @@ type Props = {
|
||||
moduleId: string;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, moduleId, projectId, workspaceSlug } = props;
|
||||
const { parentRef, moduleId, projectId, workspaceSlug, customClassName } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// states
|
||||
@@ -167,7 +168,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -17,9 +17,9 @@ type TPagesListRoot = {
|
||||
export const PagesListRoot: FC<TPagesListRoot> = observer((props) => {
|
||||
const { pageType, storeType } = props;
|
||||
// store hooks
|
||||
const { getCurrentProjectFilteredPageIds } = usePageStore(storeType);
|
||||
const { getCurrentProjectFilteredPageIdsByTab } = usePageStore(storeType);
|
||||
// derived values
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||
|
||||
if (!filteredPageIds) return <></>;
|
||||
return (
|
||||
|
||||
@@ -26,13 +26,13 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } =
|
||||
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||
usePageStore(storeType);
|
||||
const { toggleCreatePageModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const pageIds = getCurrentProjectPageIds(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
||||
const pageIds = getCurrentProjectPageIdsByTab(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
|
||||
@@ -22,10 +22,11 @@ type Props = {
|
||||
projectId: string;
|
||||
view: IProjectView;
|
||||
workspaceSlug: string;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, projectId, view, workspaceSlug } = props;
|
||||
const { parentRef, projectId, view, workspaceSlug, customClassName } = props;
|
||||
// states
|
||||
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
||||
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
||||
@@ -95,7 +96,7 @@ export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -37,8 +37,9 @@ export interface IProjectPageStore {
|
||||
isAnyPageAvailable: boolean;
|
||||
canCurrentUserCreatePage: boolean;
|
||||
// helper actions
|
||||
getCurrentProjectPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
getCurrentProjectFilteredPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
getCurrentProjectPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
getCurrentProjectPageIds: (projectId: string) => string[];
|
||||
getCurrentProjectFilteredPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
getPageById: (pageId: string) => TProjectPage | undefined;
|
||||
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
|
||||
clearAllFilters: () => void;
|
||||
@@ -46,7 +47,7 @@ export interface IProjectPageStore {
|
||||
fetchPagesList: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageType: TPageNavigationTabs
|
||||
pageType?: TPageNavigationTabs
|
||||
) => Promise<TPage[] | undefined>;
|
||||
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
|
||||
createPage: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
||||
@@ -125,7 +126,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @description get the current project page ids based on the pageType
|
||||
* @param {TPageNavigationTabs} pageType
|
||||
*/
|
||||
getCurrentProjectPageIds = computedFn((pageType: TPageNavigationTabs) => {
|
||||
getCurrentProjectPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
|
||||
const { projectId } = this.store.router;
|
||||
if (!projectId) return undefined;
|
||||
// helps to filter pages based on the pageType
|
||||
@@ -137,11 +138,21 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
return pages ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the current project page ids
|
||||
* @param {string} projectId
|
||||
*/
|
||||
getCurrentProjectPageIds = computedFn((projectId: string) => {
|
||||
if (!projectId) return [];
|
||||
const pages = Object.values(this?.data || {}).filter((page) => page.project_ids?.includes(projectId));
|
||||
return pages.map((page) => page.id) as string[];
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the current project filtered page ids based on the pageType
|
||||
* @param {TPageNavigationTabs} pageType
|
||||
*/
|
||||
getCurrentProjectFilteredPageIds = computedFn((pageType: TPageNavigationTabs) => {
|
||||
getCurrentProjectFilteredPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
|
||||
const { projectId } = this.store.router;
|
||||
if (!projectId) return undefined;
|
||||
|
||||
@@ -183,11 +194,11 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
/**
|
||||
* @description fetch all the pages
|
||||
*/
|
||||
fetchPagesList = async (workspaceSlug: string, projectId: string, pageType: TPageNavigationTabs) => {
|
||||
fetchPagesList = async (workspaceSlug: string, projectId: string, pageType?: TPageNavigationTabs) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) return undefined;
|
||||
|
||||
const currentPageIds = this.getCurrentProjectPageIds(pageType);
|
||||
const currentPageIds = pageType ? this.getCurrentProjectPageIdsByTab(pageType) : undefined;
|
||||
runInAction(() => {
|
||||
this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
|
||||
this.error = undefined;
|
||||
|
||||
Reference in New Issue
Block a user