[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:
parent
2b411de1e3
commit
993c7899b6
17 changed files with 269 additions and 453 deletions
8
packages/types/src/common.d.ts
vendored
8
packages/types/src/common.d.ts
vendored
|
|
@ -26,3 +26,11 @@ export type TLogoProps = {
|
||||||
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
|
||||||
|
|
||||||
export type TFetchStatus = "partial" | "complete" | undefined;
|
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!!!
|
// FIXME: fix this!!!
|
||||||
import { Placement } from "@blueprintjs/popover2";
|
import { Placement } from "@blueprintjs/popover2";
|
||||||
|
import { ICustomSearchSelectOption } from "@plane/types";
|
||||||
|
|
||||||
export interface IDropdownProps {
|
export interface IDropdownProps {
|
||||||
customButtonClassName?: string;
|
customButtonClassName?: string;
|
||||||
|
|
@ -44,15 +45,7 @@ interface CustomSearchSelectProps {
|
||||||
onChange: any;
|
onChange: any;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
noResultsMessage?: string;
|
noResultsMessage?: string;
|
||||||
options:
|
options?: ICustomSearchSelectOption[];
|
||||||
| {
|
|
||||||
value: any;
|
|
||||||
query: string;
|
|
||||||
content: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
tooltip?: string | React.ReactNode;
|
|
||||||
}[]
|
|
||||||
| undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SingleValueProps {
|
interface SingleValueProps {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
@ -18,12 +18,18 @@ import {
|
||||||
// i18n
|
// i18n
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import {
|
||||||
|
ICustomSearchSelectOption,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
} from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
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";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -69,6 +75,8 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
|
// refs
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
// states
|
// states
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
// router
|
// router
|
||||||
|
|
@ -159,6 +167,25 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
EUserPermissionsLevel.PROJECT
|
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);
|
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -201,33 +228,29 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="component"
|
type="component"
|
||||||
component={
|
component={
|
||||||
<CustomMenu
|
<CustomSearchSelect
|
||||||
|
options={switcherOptions}
|
||||||
|
value={cycleId}
|
||||||
|
onChange={() => {}}
|
||||||
label={
|
label={
|
||||||
<>
|
<div className="flex items-center gap-1">
|
||||||
<ContrastIcon className="h-3 w-3" />
|
<SwitcherLabel name={cycleDetails?.name} LabelIcon={ContrastIcon} />
|
||||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
{workItemsCount && workItemsCount > 0 ? (
|
||||||
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
|
<Tooltip
|
||||||
{issuesCount && issuesCount > 0 ? (
|
isMobile={isMobile}
|
||||||
<Tooltip
|
tooltipContent={`There are ${workItemsCount} ${
|
||||||
isMobile={isMobile}
|
workItemsCount > 1 ? "work items" : "work item"
|
||||||
tooltipContent={`There are ${issuesCount} ${
|
} in this cycle`}
|
||||||
issuesCount > 1 ? "work items" : "work item"
|
position="bottom"
|
||||||
} 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 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">
|
</span>
|
||||||
{issuesCount}
|
</Tooltip>
|
||||||
</span>
|
) : null}
|
||||||
</Tooltip>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
className="ml-1.5 flex-shrink-0 truncate"
|
/>
|
||||||
placement="bottom-start"
|
|
||||||
>
|
|
||||||
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)}
|
|
||||||
</CustomMenu>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
@ -302,19 +325,19 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="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}
|
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>
|
</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>
|
</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.RightItem>
|
||||||
</Header>
|
</Header>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,17 @@ import {
|
||||||
EUserPermissionsLevel,
|
EUserPermissionsLevel,
|
||||||
} from "@plane/constants";
|
} from "@plane/constants";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import {
|
||||||
|
ICustomSearchSelectOption,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
} from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -155,7 +160,24 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||||
EUserPermissionsLevel.PROJECT
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -196,33 +218,29 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="component"
|
type="component"
|
||||||
component={
|
component={
|
||||||
<CustomMenu
|
<CustomSearchSelect
|
||||||
|
options={switcherOptions}
|
||||||
label={
|
label={
|
||||||
<>
|
<div className="flex items-center gap-1">
|
||||||
<DiceIcon className="h-3 w-3" />
|
<SwitcherLabel name={moduleDetails?.name} LabelIcon={DiceIcon} />
|
||||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
{workItemsCount && workItemsCount > 0 ? (
|
||||||
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
<Tooltip
|
||||||
{issuesCount && issuesCount > 0 ? (
|
isMobile={isMobile}
|
||||||
<Tooltip
|
tooltipContent={`There are ${workItemsCount} ${
|
||||||
isMobile={isMobile}
|
workItemsCount > 1 ? "work items" : "work item"
|
||||||
tooltipContent={`There are ${issuesCount} ${
|
} in this module`}
|
||||||
issuesCount > 1 ? "work items" : "work item"
|
position="bottom"
|
||||||
} 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 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">
|
</span>
|
||||||
{issuesCount}
|
</Tooltip>
|
||||||
</span>
|
) : null}
|
||||||
</Tooltip>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
className="ml-1.5 flex-shrink-0"
|
value={moduleId}
|
||||||
placement="bottom-start"
|
onChange={() => {}}
|
||||||
>
|
/>
|
||||||
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
|
||||||
</CustomMenu>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,23 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { FileText } from "lucide-react";
|
import { FileText, Layers } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TLogoProps } from "@plane/types";
|
import { ICustomSearchSelectOption } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast, Header } from "@plane/ui";
|
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||||
import { PageEditInformationPopover } from "@/components/pages";
|
import { PageEditInformationPopover } from "@/components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
|
||||||
import { getPageName } from "@/helpers/page.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
||||||
// plane web components
|
// plane web components
|
||||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { EPageStoreType, usePage } from "@/plane-web/hooks/store";
|
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||||
|
|
||||||
export interface IPagesHeaderProps {
|
export interface IPagesHeaderProps {
|
||||||
showButton?: boolean;
|
showButton?: boolean;
|
||||||
|
|
@ -29,42 +25,36 @@ export interface IPagesHeaderProps {
|
||||||
|
|
||||||
export const PageDetailsHeader = observer(() => {
|
export const PageDetailsHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, pageId } = useParams();
|
const { workspaceSlug, pageId, projectId } = useParams();
|
||||||
// state
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentProjectDetails, loader } = useProject();
|
const { currentProjectDetails, loader } = useProject();
|
||||||
const page = usePage({
|
const page = usePage({
|
||||||
pageId: pageId?.toString() ?? "",
|
pageId: pageId?.toString() ?? "",
|
||||||
storeType: EPageStoreType.PROJECT,
|
storeType: EPageStoreType.PROJECT,
|
||||||
});
|
});
|
||||||
if (!page) return null;
|
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
|
||||||
// derived values
|
// derived values
|
||||||
const { name, logo_props, updatePageLogo, isContentEditable } = page;
|
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
||||||
// use platform
|
|
||||||
const { isMobile } = usePlatformOS();
|
|
||||||
|
|
||||||
const handlePageLogoUpdate = async (data: TLogoProps) => {
|
if (!page) return null;
|
||||||
if (data) {
|
const switcherOptions = projectPageIds
|
||||||
updatePageLogo(data)
|
.map((id) => {
|
||||||
.then(() => {
|
const _page = id === pageId ? page : getPageById(id);
|
||||||
setToast({
|
if (!_page) return;
|
||||||
type: TOAST_TYPE.SUCCESS,
|
const pageLink = `/${workspaceSlug}/projects/${projectId}/pages/${_page.id}`;
|
||||||
title: "Success!",
|
return {
|
||||||
message: "Logo Updated successfully.",
|
value: _page.id,
|
||||||
});
|
query: _page.name,
|
||||||
})
|
content: (
|
||||||
.catch(() => {
|
<Link href={pageLink} className="flex gap-2 items-center justify-between">
|
||||||
setToast({
|
<SwitcherLabel logo_props={_page.logo_props} name={_page.name} LabelIcon={Layers} />
|
||||||
type: TOAST_TYPE.ERROR,
|
</Link>
|
||||||
title: "Error!",
|
),
|
||||||
message: "Something went wrong. Please try again.",
|
};
|
||||||
});
|
})
|
||||||
});
|
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageTitle = getPageName(name);
|
if (!page) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
|
|
@ -99,60 +89,14 @@ export const PageDetailsHeader = observer(() => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="component"
|
||||||
link={
|
component={
|
||||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
<CustomSearchSelect
|
||||||
<div className="flex flex-wrap items-center gap-2.5">
|
value={pageId}
|
||||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
options={switcherOptions}
|
||||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden">
|
label={<SwitcherLabel logo_props={page.logo_props} name={page.name} LabelIcon={Layers} />}
|
||||||
<EmojiIconPicker
|
onChange={() => {}}
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||||
|
// plane web hooks
|
||||||
|
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||||
// local components
|
// local components
|
||||||
import { PageDetailsHeader } from "./header";
|
import { PageDetailsHeader } from "./header";
|
||||||
|
|
||||||
export default function ProjectPageDetailsLayout({ children }: { children: React.ReactNode }) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppHeader header={<PageDetailsHeader />} />
|
<AppHeader header={<PageDetailsHeader />} />
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,21 @@ import {
|
||||||
EUserPermissionsLevel,
|
EUserPermissionsLevel,
|
||||||
} from "@plane/constants";
|
} from "@plane/constants";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import {
|
||||||
|
ICustomSearchSelectOption,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
} from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, Tooltip, Header } from "@plane/ui";
|
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { ViewQuickActions } from "@/components/views";
|
import { ViewQuickActions } from "@/components/views";
|
||||||
// helpers
|
// helpers
|
||||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import {
|
import {
|
||||||
useCommandPalette,
|
useCommandPalette,
|
||||||
|
|
@ -143,6 +147,23 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
|
|
||||||
if (!viewDetails) return;
|
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 (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<Header.LeftItem>
|
<Header.LeftItem>
|
||||||
|
|
@ -161,42 +182,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="component"
|
type="component"
|
||||||
component={
|
component={
|
||||||
<CustomMenu
|
<CustomSearchSelect
|
||||||
label={
|
options={switcherOptions}
|
||||||
<>
|
value={viewId}
|
||||||
{viewDetails?.logo_props?.in_use ? (
|
label={<SwitcherLabel logo_props={viewDetails.logo_props} name={viewDetails.name} LabelIcon={Layers} />}
|
||||||
<Logo logo={viewDetails.logo_props} size={12} type="lucide" />
|
onChange={() => {}}
|
||||||
) : (
|
/>
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</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.LeftItem>
|
||||||
<Header.RightItem>
|
<Header.RightItem className="items-center">
|
||||||
{!viewDetails?.is_locked ? (
|
{!viewDetails?.is_locked ? (
|
||||||
<>
|
<>
|
||||||
<LayoutSelection
|
<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.RightItem>
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ export * from "./logo";
|
||||||
export * from "./pro-icon";
|
export * from "./pro-icon";
|
||||||
export * from "./count-chip";
|
export * from "./count-chip";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
|
export * from "./switcher-label";
|
||||||
|
|
|
||||||
27
web/core/components/common/switcher-label.tsx
Normal file
27
web/core/components/common/switcher-label.tsx
Normal file
|
|
@ -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 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) => {
|
const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
|
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" />
|
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<div className="flex items-start justify-between gap-3 pt-2">
|
<div className="flex items-start justify-between gap-3 pt-2">
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ type Props = {
|
||||||
cycleId: string;
|
cycleId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
customClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||||
const { parentRef, cycleId, projectId, workspaceSlug } = props;
|
const { parentRef, cycleId, projectId, workspaceSlug, customClassName } = props;
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// states
|
// states
|
||||||
|
|
@ -188,7 +189,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
<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) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
if (item.shouldRender === false) return null;
|
if (item.shouldRender === false) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react";
|
||||||
ArchiveRestoreIcon,
|
|
||||||
CalendarClock,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
Info,
|
|
||||||
LinkIcon,
|
|
||||||
Plus,
|
|
||||||
SquareUser,
|
|
||||||
Trash2,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// plane types
|
// plane types
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,18 +19,7 @@ import {
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
import {
|
import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
|
||||||
CustomMenu,
|
|
||||||
Loader,
|
|
||||||
LayersIcon,
|
|
||||||
CustomSelect,
|
|
||||||
ModuleStatusIcon,
|
|
||||||
TOAST_TYPE,
|
|
||||||
setToast,
|
|
||||||
ArchiveIcon,
|
|
||||||
TextArea,
|
|
||||||
} from "@plane/ui";
|
|
||||||
import { copyUrlToClipboard } from "@plane/utils";
|
|
||||||
// components
|
// components
|
||||||
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
|
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
|
||||||
import {
|
import {
|
||||||
|
|
@ -55,7 +33,6 @@ import {
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store";
|
import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
|
||||||
// plane web constants
|
// plane web constants
|
||||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
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 [moduleLinkModal, setModuleLinkModal] = useState(false);
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } =
|
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
||||||
useModule();
|
const { captureModuleEvent, captureEvent } = useEventTracker();
|
||||||
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
|
|
||||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const moduleState = moduleDetails?.status?.toLocaleLowerCase();
|
|
||||||
const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
|
|
||||||
|
|
||||||
const areEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId.toString());
|
const areEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId.toString());
|
||||||
const estimateType = areEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
const estimateType = areEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
||||||
const isEstimatePointValid = estimateType && estimateType?.type == EEstimateSystem.POINTS ? true : false;
|
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) => {
|
const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => {
|
||||||
submitChanges({
|
submitChanges({
|
||||||
start_date: startDate ? renderFormattedPayloadDate(startDate) : null,
|
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(() => {
|
useEffect(() => {
|
||||||
if (moduleDetails)
|
if (moduleDetails)
|
||||||
reset({
|
reset({
|
||||||
|
|
@ -309,56 +239,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
|
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ type Props = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
customClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||||
const { parentRef, moduleId, projectId, workspaceSlug } = props;
|
const { parentRef, moduleId, projectId, workspaceSlug, customClassName } = props;
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// states
|
// states
|
||||||
|
|
@ -167,7 +168,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
if (item.shouldRender === false) return null;
|
if (item.shouldRender === false) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ type TPagesListRoot = {
|
||||||
export const PagesListRoot: FC<TPagesListRoot> = observer((props) => {
|
export const PagesListRoot: FC<TPagesListRoot> = observer((props) => {
|
||||||
const { pageType, storeType } = props;
|
const { pageType, storeType } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getCurrentProjectFilteredPageIds } = usePageStore(storeType);
|
const { getCurrentProjectFilteredPageIdsByTab } = usePageStore(storeType);
|
||||||
// derived values
|
// derived values
|
||||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||||
|
|
||||||
if (!filteredPageIds) return <></>;
|
if (!filteredPageIds) return <></>;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||||
// plane hooks
|
// plane hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } =
|
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||||
usePageStore(storeType);
|
usePageStore(storeType);
|
||||||
const { toggleCreatePageModal } = useCommandPalette();
|
const { toggleCreatePageModal } = useCommandPalette();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// derived values
|
// derived values
|
||||||
const pageIds = getCurrentProjectPageIds(pageType);
|
const pageIds = getCurrentProjectPageIdsByTab(pageType);
|
||||||
const filteredPageIds = getCurrentProjectFilteredPageIds(pageType);
|
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||||
const canPerformEmptyStateActions = allowPermissions(
|
const canPerformEmptyStateActions = allowPermissions(
|
||||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||||
EUserPermissionsLevel.PROJECT
|
EUserPermissionsLevel.PROJECT
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
view: IProjectView;
|
view: IProjectView;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
customClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
const { parentRef, projectId, view, workspaceSlug } = props;
|
const { parentRef, projectId, view, workspaceSlug, customClassName } = props;
|
||||||
// states
|
// states
|
||||||
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
||||||
const [deleteViewModal, setDeleteViewModal] = 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)} />
|
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||||
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
|
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
|
||||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
if (item.shouldRender === false) return null;
|
if (item.shouldRender === false) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ export interface IProjectPageStore {
|
||||||
isAnyPageAvailable: boolean;
|
isAnyPageAvailable: boolean;
|
||||||
canCurrentUserCreatePage: boolean;
|
canCurrentUserCreatePage: boolean;
|
||||||
// helper actions
|
// helper actions
|
||||||
getCurrentProjectPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
getCurrentProjectPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||||
getCurrentProjectFilteredPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
getCurrentProjectPageIds: (projectId: string) => string[];
|
||||||
|
getCurrentProjectFilteredPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||||
getPageById: (pageId: string) => TProjectPage | undefined;
|
getPageById: (pageId: string) => TProjectPage | undefined;
|
||||||
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
|
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
|
||||||
clearAllFilters: () => void;
|
clearAllFilters: () => void;
|
||||||
|
|
@ -46,7 +47,7 @@ export interface IProjectPageStore {
|
||||||
fetchPagesList: (
|
fetchPagesList: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
pageType: TPageNavigationTabs
|
pageType?: TPageNavigationTabs
|
||||||
) => Promise<TPage[] | undefined>;
|
) => Promise<TPage[] | undefined>;
|
||||||
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
|
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
|
||||||
createPage: (pageData: Partial<TPage>) => 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
|
* @description get the current project page ids based on the pageType
|
||||||
* @param {TPageNavigationTabs} pageType
|
* @param {TPageNavigationTabs} pageType
|
||||||
*/
|
*/
|
||||||
getCurrentProjectPageIds = computedFn((pageType: TPageNavigationTabs) => {
|
getCurrentProjectPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
|
||||||
const { projectId } = this.store.router;
|
const { projectId } = this.store.router;
|
||||||
if (!projectId) return undefined;
|
if (!projectId) return undefined;
|
||||||
// helps to filter pages based on the pageType
|
// helps to filter pages based on the pageType
|
||||||
|
|
@ -137,11 +138,21 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||||
return pages ?? undefined;
|
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
|
* @description get the current project filtered page ids based on the pageType
|
||||||
* @param {TPageNavigationTabs} pageType
|
* @param {TPageNavigationTabs} pageType
|
||||||
*/
|
*/
|
||||||
getCurrentProjectFilteredPageIds = computedFn((pageType: TPageNavigationTabs) => {
|
getCurrentProjectFilteredPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
|
||||||
const { projectId } = this.store.router;
|
const { projectId } = this.store.router;
|
||||||
if (!projectId) return undefined;
|
if (!projectId) return undefined;
|
||||||
|
|
||||||
|
|
@ -183,11 +194,11 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||||
/**
|
/**
|
||||||
* @description fetch all the pages
|
* @description fetch all the pages
|
||||||
*/
|
*/
|
||||||
fetchPagesList = async (workspaceSlug: string, projectId: string, pageType: TPageNavigationTabs) => {
|
fetchPagesList = async (workspaceSlug: string, projectId: string, pageType?: TPageNavigationTabs) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId) return undefined;
|
if (!workspaceSlug || !projectId) return undefined;
|
||||||
|
|
||||||
const currentPageIds = this.getCurrentProjectPageIds(pageType);
|
const currentPageIds = pageType ? this.getCurrentProjectPageIdsByTab(pageType) : undefined;
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
|
this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue