[WEB-4050] feat: breadcrumbs revamp (#7188)

* chore: project feature enum added

* feat: revamp breadcrumb and add navigation dropdown component

* chore: custom search select component refactoring

* chore: breadcrumb stories added

* chore: switch label and breadcrumb link component refactor

* chore: project navigation helper function added

* chore: common breadcrumb component added

* chore: breadcrumb refactoring

* chore: code refactor

* chore: code refactor

* fix: build error

* fix: nprogress and button tooltip

* chore: code refactor

* chore: workspace view breadcrumb improvements

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

---------

Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2025-06-19 17:17:14 +05:30 committed by GitHub
parent 64fd0b2830
commit 2b7a17b484
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1251 additions and 581 deletions

View file

@ -67,9 +67,8 @@ export const InstanceHeader: FC = observer(() => {
{breadcrumbItems.length >= 0 && ( {breadcrumbItems.length >= 0 && (
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
href="/general/" href="/general/"
label="Settings" label="Settings"
@ -80,10 +79,9 @@ export const InstanceHeader: FC = observer(() => {
{breadcrumbItems.map( {breadcrumbItems.map(
(item) => (item) =>
item.title && ( item.title && (
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
key={item.title} key={item.title}
type="text" component={<BreadcrumbLink href={item.href} label={item.title} />}
link={<BreadcrumbLink href={item.href} label={item.title} />}
/> />
) )
)} )}

View file

@ -149,3 +149,12 @@ export const DEFAULT_PROJECT_FORM_VALUES: Partial<IProject> = {
network: 2, network: 2,
project_lead: null, project_lead: null,
}; };
export enum EProjectFeatureKey {
WORK_ITEMS = "work_items",
CYCLES = "cycles",
MODULES = "modules",
VIEWS = "views",
PAGES = "pages",
INTAKE = "intake",
}

View file

@ -0,0 +1,233 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Home, Settings, Briefcase, GridIcon, Layers2, FileIcon } from "lucide-react";
import * as React from "react";
import { ContrastIcon, EpicIcon, LayersIcon } from "../icons";
import { Breadcrumbs } from "./breadcrumbs";
import { BreadcrumbNavigationDropdown } from "./navigation-dropdown";
const meta: Meta<typeof Breadcrumbs> = {
title: "UI/Breadcrumbs",
component: Breadcrumbs,
tags: ["autodocs"],
argTypes: {
isLoading: {
control: "boolean",
description: "Shows loading state of breadcrumbs",
},
onBack: {
action: "onBack",
description: "Callback function when back button is clicked",
},
},
};
type TBreadcrumbBlockProps = {
href?: string;
label?: string;
icon?: React.ReactNode;
disableTooltip?: boolean;
};
// TODO: remove this component and use web Link component
const BreadcrumbBlock: React.FC<TBreadcrumbBlockProps> = (props) => {
const { label, icon, disableTooltip = false } = props;
return (
<>
<Breadcrumbs.ItemWrapper label={label} disableTooltip={disableTooltip}>
{icon && <div className="flex size-4 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>}
{label && <div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>}
</Breadcrumbs.ItemWrapper>
</>
);
};
export default meta;
type Story = StoryObj<typeof Breadcrumbs>;
export const Default: Story = {
args: {
children: [
<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />,
<Breadcrumbs.Item key="projects" component={<BreadcrumbBlock href="/projects" label="Projects" />} />,
<Breadcrumbs.Item
key="current"
component={<BreadcrumbBlock href="/projects/current" label="Current Project" />}
/>,
],
},
};
export const WithLoading: Story = {
args: {
isLoading: true,
children: [
<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />,
<Breadcrumbs.Item key="projects" component={<BreadcrumbBlock href="/projects" label="Projects" />} />,
],
},
};
export const WithCustomComponent: Story = {
args: {
children: [
<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />,
<Breadcrumbs.Item
key="custom"
component={
<div className="flex items-center gap-2">
<span className="size-4 rounded-full bg-blue-500" />
<span>Custom Component</span>
</div>
}
/>,
],
},
};
export const SingleItem: Story = {
args: {
children: [<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />],
},
};
export const WithNavigationDropdown: Story = {
args: {
children: [
<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />,
<Breadcrumbs.Item
key="projects"
component={
<BreadcrumbNavigationDropdown
selectedItemKey="project-1"
navigationItems={[
{
key: "project-1",
title: "Project Alpha",
action: () => console.log("Project Alpha selected"),
},
{
key: "project-2",
title: "Project Beta",
action: () => console.log("Project Beta selected"),
},
{
key: "project-3",
title: "Project Gamma",
action: () => console.log("Project Gamma selected"),
},
]}
/>
}
showSeparator={false}
/>,
<Breadcrumbs.Item key="settings" component={<BreadcrumbBlock href="/settings" label="Settings" />} />,
],
},
};
export const WithNavigationDropdownAndIcons: Story = {
args: {
children: [
<Breadcrumbs.Item
key="home"
component={<BreadcrumbBlock href="/" label="Home" icon={<Home className="size-3.5" />} />}
/>,
<Breadcrumbs.Item
key="projects"
component={
<BreadcrumbNavigationDropdown
selectedItemKey="project-1"
navigationItems={[
{
key: "project-1",
title: "Project Alpha",
icon: Briefcase,
action: () => console.log("Project Alpha selected"),
},
{
key: "project-2",
title: "Project Beta",
icon: Briefcase,
// disabled: true,
action: () => console.log("Project Beta selected"),
},
{
key: "project-3",
title: "Project Gamma",
icon: Briefcase,
action: () => console.log("Project Gamma selected"),
},
]}
/>
}
showSeparator={false}
/>,
<Breadcrumbs.Item
key="features"
component={
<BreadcrumbNavigationDropdown
selectedItemKey="feature-1"
navigationItems={[
{
key: "feature-1",
title: "Epics",
icon: EpicIcon,
action: () => console.log("Feature Alpha selected"),
},
{
key: "feature-2",
title: "Work items",
icon: LayersIcon,
// disabled: true,
action: () => console.log("Feature Beta selected"),
},
{
key: "feature-3",
title: "Cycles",
icon: ContrastIcon,
action: () => console.log("Feature Gamma selected"),
},
{
key: "feature-3",
title: "Modules",
icon: GridIcon,
action: () => console.log("Feature Gamma selected"),
},
{
key: "feature-3",
title: "Views",
icon: Layers2,
action: () => console.log("Feature Gamma selected"),
},
{
key: "feature-3",
title: "Pages",
icon: FileIcon,
action: () => console.log("Feature Gamma selected"),
},
]}
/>
}
showSeparator={false}
/>,
<Breadcrumbs.Item
key="settings"
component={<BreadcrumbBlock href="/settings" label="Settings" icon={<Settings className="size-3.5" />} />}
isLast
/>,
],
},
};

View file

@ -1,13 +1,25 @@
import * as React from "react";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import * as React from "react";
import { cn } from "../../helpers";
import { Tooltip } from "../tooltip";
type BreadcrumbsProps = { type BreadcrumbsProps = {
className?: string;
children: React.ReactNode; children: React.ReactNode;
onBack?: () => void; onBack?: () => void;
isLoading?: boolean; isLoading?: boolean;
}; };
const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) => { export const BreadcrumbItemLoader = () => (
<div className="flex items-center gap-2 h-7 animate-pulse">
<div className="group h-full flex items-center gap-2 rounded px-2 py-1 text-sm font-medium">
<span className="h-full w-5 bg-custom-background-80 rounded" />
<span className="h-full w-16 bg-custom-background-80 rounded" />
</div>
</div>
);
const Breadcrumbs = ({ className, children, onBack, isLoading = false }: BreadcrumbsProps) => {
const [isSmallScreen, setIsSmallScreen] = React.useState(false); const [isSmallScreen, setIsSmallScreen] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
@ -22,35 +34,31 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
const childrenArray = React.Children.toArray(children); const childrenArray = React.Children.toArray(children);
const BreadcrumbItemLoader = (
<div className="flex items-center gap-1 animate-pulse">
<span className="h-5 w-5 bg-custom-background-80 rounded" />
<span className="h-5 w-16 bg-custom-background-80 rounded" />
</div>
);
return ( return (
<div className="flex items-center space-x-2 overflow-hidden"> <div className={cn("flex items-center overflow-hidden gap-0.5 flex-grow", className)}>
{!isSmallScreen && ( {!isSmallScreen && (
<> <>
{childrenArray.map((child, index) => ( {childrenArray.map((child, index) => {
<React.Fragment key={index}> if (isLoading) {
{index > 0 && !isSmallScreen && ( return (
<div className="flex items-center gap-2.5"> <>
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" /> <BreadcrumbItemLoader />
</div> </>
)} );
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? "hidden sm:flex" : "flex"}`}> }
{isLoading ? BreadcrumbItemLoader : child} if (React.isValidElement<BreadcrumbItemProps>(child)) {
</div> return React.cloneElement(child, {
</React.Fragment> isLast: index === childrenArray.length - 1,
))} });
}
return child;
})}
</> </>
)} )}
{isSmallScreen && childrenArray.length > 1 && ( {isSmallScreen && childrenArray.length > 1 && (
<> <>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5 p-1">
{onBack && ( {onBack && (
<span onClick={onBack} className="text-custom-text-200"> <span onClick={onBack} className="text-custom-text-200">
... ...
@ -58,8 +66,16 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
)} )}
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" /> <ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div> </div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5 p-1">
{isLoading ? BreadcrumbItemLoader : childrenArray[childrenArray.length - 1]} {isLoading ? (
<BreadcrumbItemLoader />
) : React.isValidElement(childrenArray[childrenArray.length - 1]) ? (
React.cloneElement(childrenArray[childrenArray.length - 1] as React.ReactElement, {
isLast: true,
})
) : (
childrenArray[childrenArray.length - 1]
)}
</div> </div>
</> </>
)} )}
@ -68,17 +84,107 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
); );
}; };
type Props = { // breadcrumb item
type?: "text" | "component"; type BreadcrumbItemProps = {
component?: React.ReactNode; component?: React.ReactNode;
link?: JSX.Element; showSeparator?: boolean;
isLast?: boolean;
}; };
const BreadcrumbItem: React.FC<Props> = (props) => { const BreadcrumbItem: React.FC<BreadcrumbItemProps> = (props) => {
const { type = "text", component, link } = props; const { component, showSeparator = true, isLast = false } = props;
return <>{type !== "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>; return (
<div className="flex items-center gap-0.5 h-6">
{component}
{showSeparator && !isLast && <BreadcrumbSeparator />}
</div>
);
}; };
Breadcrumbs.BreadcrumbItem = BreadcrumbItem; // breadcrumb icon
type BreadcrumbIconProps = {
children: React.ReactNode;
className?: string;
};
export { Breadcrumbs, BreadcrumbItem }; const BreadcrumbIcon: React.FC<BreadcrumbIconProps> = (props) => {
const { children, className } = props;
return <div className={cn("flex size-4 items-center justify-start overflow-hidden", className)}>{children}</div>;
};
// breadcrumb label
type BreadcrumbLabelProps = {
children: React.ReactNode;
className?: string;
};
const BreadcrumbLabel: React.FC<BreadcrumbLabelProps> = (props) => {
const { children, className } = props;
return (
<div className={cn("relative line-clamp-1 block max-w-[150px] overflow-hidden truncate", className)}>
{children}
</div>
);
};
// breadcrumb separator
type BreadcrumbSeparatorProps = {
className?: string;
containerClassName?: string;
iconClassName?: string;
showDivider?: boolean;
};
const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = (props) => {
const { className, containerClassName, iconClassName, showDivider = false } = props;
return (
<div className={cn("relative flex items-center justify-center h-full px-1.5 py-1", className)}>
{showDivider && <span className="absolute -left-0.5 top-0 h-full w-[1.8px] bg-custom-background-100" />}
<div
className={cn(
"flex items-center justify-center flex-shrink-0 rounded text-custom-text-400 transition-all",
containerClassName
)}
>
<ChevronRight className={cn("h-3.5 w-3.5 flex-shrink-0", iconClassName)} />
</div>
</div>
);
};
// breadcrumb wrapper
type BreadcrumbItemWrapperProps = {
label?: string;
disableTooltip?: boolean;
children: React.ReactNode;
className?: string;
type?: "link" | "text";
isLast?: boolean;
};
const BreadcrumbItemWrapper: React.FC<BreadcrumbItemWrapperProps> = (props) => {
const { label, disableTooltip = false, children, className, type = "link", isLast = false } = props;
return (
<Tooltip tooltipContent={label} position="bottom" disabled={!label || label === "" || disableTooltip}>
<div
className={cn(
"group h-full flex items-center gap-2 rounded px-1.5 py-1 text-sm font-medium text-custom-text-300 cursor-default",
{
"hover:text-custom-text-100 hover:bg-custom-background-90 cursor-pointer": type === "link" && !isLast,
},
className
)}
>
{children}
</div>
</Tooltip>
);
};
Breadcrumbs.Item = BreadcrumbItem;
Breadcrumbs.Icon = BreadcrumbIcon;
Breadcrumbs.Label = BreadcrumbLabel;
Breadcrumbs.Separator = BreadcrumbSeparator;
Breadcrumbs.ItemWrapper = BreadcrumbItemWrapper;
export { Breadcrumbs, BreadcrumbItem, BreadcrumbIcon, BreadcrumbLabel, BreadcrumbSeparator, BreadcrumbItemWrapper };

View file

@ -1,2 +1,3 @@
export * from "./breadcrumbs"; export * from "./breadcrumbs";
export * from "./navigation-dropdown"; export * from "./navigation-dropdown";
export * from "./navigation-search-dropdown";

View file

@ -1,42 +1,54 @@
"use client"; "use client";
import { CheckIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { CheckIcon, ChevronDownIcon } from "lucide-react"; import { cn } from "../../helpers";
// ui // ui
import { CustomMenu, TContextMenuItem } from "../dropdowns"; import { CustomMenu, TContextMenuItem } from "../dropdowns";
// helpers import { Tooltip } from "../tooltip";
import { cn } from "../../helpers"; import { Breadcrumbs } from "./breadcrumbs";
type TBreadcrumbNavigationDropdownProps = { type TBreadcrumbNavigationDropdownProps = {
selectedItemKey: string; selectedItemKey: string;
navigationItems: TContextMenuItem[]; navigationItems: TContextMenuItem[];
navigationDisabled?: boolean; navigationDisabled?: boolean;
handleOnClick?: () => void;
isLast?: boolean;
}; };
export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => { export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => {
const { selectedItemKey, navigationItems, navigationDisabled = false } = props; const { selectedItemKey, navigationItems, navigationDisabled = false, handleOnClick, isLast = false } = props;
const [isOpen, setIsOpen] = React.useState(false);
// derived values // derived values
const selectedItem = navigationItems.find((item) => item.key === selectedItemKey); const selectedItem = navigationItems.find((item) => item.key === selectedItemKey);
const selectedItemIcon = selectedItem?.icon ? ( const selectedItemIcon = selectedItem?.icon ? (
<selectedItem.icon className={cn("size-3.5", selectedItem.iconClassName)} /> <selectedItem.icon className={cn("size-4", selectedItem.iconClassName)} />
) : undefined; ) : undefined;
// if no selected item, return null // if no selected item, return null
if (!selectedItem) return null; if (!selectedItem) return null;
const NavigationButton = ({ className }: { className?: string }) => ( const NavigationButton = () => (
<li <Tooltip tooltipContent={selectedItem.title} position="bottom" disabled={isOpen}>
className={cn( <button
"flex items-center justify-center cursor-default text-sm font-medium text-custom-text-200 group-hover:text-custom-text-100 outline-none", onClick={(e) => {
className if (!isLast) {
)} e.preventDefault();
tabIndex={-1} e.stopPropagation();
> handleOnClick?.();
{selectedItemIcon && ( }
<div className="flex h-5 w-5 items-center justify-start overflow-hidden">{selectedItemIcon}</div> }}
)} className={cn(
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{selectedItem.title}</div> "group h-full flex items-center gap-2 px-1.5 py-1 text-sm font-medium text-custom-text-300 cursor-pointer rounded rounded-r-none",
</li> {
"hover:bg-custom-background-80 hover:text-custom-text-100": !isLast,
}
)}
>
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
</button>
</Tooltip>
); );
if (navigationDisabled) { if (navigationDisabled) {
@ -46,13 +58,37 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
return ( return (
<CustomMenu <CustomMenu
customButton={ customButton={
<div className="group flex items-center gap-1.5"> <>
<NavigationButton className="cursor-pointer" /> <NavigationButton />
<ChevronDownIcon className="size-4 text-custom-text-200 group-hover:text-custom-text-100" /> <Breadcrumbs.Separator
</div> className={cn("rounded-r", {
"bg-custom-background-80": isOpen && !isLast,
"hover:bg-custom-background-80": !isLast,
})}
containerClassName="p-0"
iconClassName={cn("group-hover:rotate-90 hover:text-custom-text-100", {
"text-custom-text-100": isOpen,
"rotate-90": isOpen || isLast,
})}
showDivider={!isLast}
/>
</>
} }
placement="bottom-start" placement="bottom-start"
className="h-full rounded"
customButtonClassName={cn(
"group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full rounded",
{
"bg-custom-background-90": isOpen,
}
)}
closeOnSelect closeOnSelect
menuButtonOnClick={() => {
setIsOpen(!isOpen);
}}
onMenuClose={() => {
setIsOpen(false);
}}
> >
{navigationItems.map((item) => { {navigationItems.map((item) => {
if (item.shouldRender === false) return null; if (item.shouldRender === false) return null;
@ -74,7 +110,7 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
)} )}
disabled={item.disabled} disabled={item.disabled}
> >
{item.icon && <item.icon className={cn("size-3.5", item.iconClassName)} />} {item.icon && <item.icon className={cn("size-4 flex-shrink-0", item.iconClassName)} />}
<div className="w-full"> <div className="w-full">
<h5>{item.title}</h5> <h5>{item.title}</h5>
{item.description && ( {item.description && (

View file

@ -0,0 +1,96 @@
import * as React from "react";
import { useState } from "react";
import { ICustomSearchSelectOption } from "@plane/types";
import { cn } from "../../helpers";
import { CustomSearchSelect } from "../dropdowns";
import { Tooltip } from "../tooltip";
import { Breadcrumbs } from "./breadcrumbs";
type TBreadcrumbNavigationSearchDropdownProps = {
icon?: React.JSX.Element;
title?: string;
selectedItem: string;
navigationItems: ICustomSearchSelectOption[];
onChange?: (value: string) => void;
navigationDisabled?: boolean;
isLast?: boolean;
handleOnClick?: () => void;
disableRootHover?: boolean;
};
export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationSearchDropdownProps> = (props) => {
const {
icon,
title,
selectedItem,
navigationItems,
onChange,
navigationDisabled = false,
isLast = false,
handleOnClick,
} = props;
// state
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
return (
<CustomSearchSelect
onOpen={() => {
setIsDropdownOpen(true);
}}
onClose={() => {
setIsDropdownOpen(false);
}}
options={navigationItems}
value={selectedItem}
onChange={(value: string) => {
if (value !== selectedItem) {
onChange?.(value);
}
}}
customButton={
<>
<Tooltip tooltipContent={title} position="bottom">
<button
onClick={(e) => {
if (!isLast) {
e.preventDefault();
e.stopPropagation();
handleOnClick?.();
}
}}
className={cn(
"group h-full flex items-center gap-2 px-1.5 py-1 text-sm font-medium text-custom-text-300 cursor-pointer rounded rounded-r-none",
{
"hover:bg-custom-background-80 hover:text-custom-text-100": !isLast,
}
)}
>
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{title}</Breadcrumbs.Label>
</button>
</Tooltip>
<Breadcrumbs.Separator
className={cn("rounded-r", {
"bg-custom-background-80": isDropdownOpen && !isLast,
"hover:bg-custom-background-80": !isLast,
})}
containerClassName="p-0"
iconClassName={cn("group-hover:rotate-90 hover:text-custom-text-100", {
"text-custom-text-100": isDropdownOpen,
"rotate-90": isDropdownOpen || isLast,
})}
showDivider={!isLast}
/>
</>
}
disabled={navigationDisabled}
className="h-full rounded"
customButtonClassName={cn(
"group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full rounded",
{
"bg-custom-background-90": isDropdownOpen,
}
)}
/>
);
};

View file

@ -61,6 +61,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
const openDropdown = () => { const openDropdown = () => {
setIsOpen(true); setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
if (onOpen) onOpen();
}; };
const closeDropdown = () => { const closeDropdown = () => {
@ -95,11 +96,14 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
<button <button
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
className={`flex w-full items-center justify-between gap-1 text-xs ${ className={cn(
disabled "flex w-full items-center justify-between gap-1 text-xs",
? "cursor-not-allowed text-custom-text-200" {
: "cursor-pointer hover:bg-custom-background-80" "cursor-not-allowed text-custom-text-200": disabled,
} ${customButtonClassName}`} "cursor-pointer hover:bg-custom-background-80": !disabled,
},
customButtonClassName
)}
onClick={toggleDropdown} onClick={toggleDropdown}
> >
{customButton} {customButton}

View file

@ -38,7 +38,10 @@ const Header = (props: HeaderProps) => {
const LeftItem = (props: HeaderProps) => ( const LeftItem = (props: HeaderProps) => (
<div <div
className={cn("flex flex-wrap items-center gap-2 overflow-ellipsis whitespace-nowrap max-w-[80%]", props.className)} className={cn(
"flex flex-wrap items-center gap-2 overflow-ellipsis whitespace-nowrap max-w-[80%] flex-grow",
props.className
)}
> >
{props.children} {props.children}
</div> </div>

View file

@ -14,18 +14,17 @@ export const WorkspaceActiveCycleHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={ <BreadcrumbLink
<BreadcrumbLink label={t("active_cycles")}
label={t("active_cycles")} icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />} />
/> }
} />
/> </Breadcrumbs>
</Breadcrumbs> <UpgradeBadge size="md" />
<UpgradeBadge size="md" /> </Header.LeftItem>
</Header.LeftItem> </Header>
</Header>
); );
}); });

View file

@ -14,9 +14,8 @@ export const WorkspaceAnalyticsHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={t("workspace_analytics.label")} label={t("workspace_analytics.label")}
icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />}

View file

@ -2,23 +2,22 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Briefcase } from "lucide-react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { EProjectFeatureKey } from "@plane/constants";
import { Breadcrumbs, LayersIcon, Header, Logo } from "@plane/ui"; import { Breadcrumbs, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { IssueDetailQuickActions } from "@/components/issues"; import { IssueDetailQuickActions } from "@/components/issues";
// hooks // hooks
import { useIssueDetail, useProject } from "@/hooks/store"; import { useIssueDetail, useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
export const ProjectIssueDetailsHeader = observer(() => { export const ProjectIssueDetailsHeader = observer(() => {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, workItem } = useParams(); const { workspaceSlug, workItem } = useParams();
// store hooks // store hooks
const { t } = useTranslation();
const { getProjectById, loader } = useProject(); const { getProjectById, loader } = useProject();
const { const {
issue: { getIssueById, getIssueIdByIdentifier }, issue: { getIssueById, getIssueIdByIdentifier },
@ -34,53 +33,20 @@ export const ProjectIssueDetailsHeader = observer(() => {
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<div> <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <CommonProjectBreadcrumbs
<Breadcrumbs.BreadcrumbItem workspaceSlug={workspaceSlug?.toString()}
type="text" projectId={projectId?.toString()}
link={ featureKey={EProjectFeatureKey.WORK_ITEMS}
<BreadcrumbLink />
label={projectDetails?.name ?? "Project"} <Breadcrumbs.Item
icon={ component={
projectDetails ? ( <BreadcrumbLink
projectDetails && ( label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""}
<span className="grid place-items-center flex-shrink-0 h-4 w-4"> />
<Logo logo={projectDetails?.logo_props} size={16} /> }
</span> />
) </Breadcrumbs>
) : (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Briefcase className="h-4 w-4" />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/issues`}
label={t("common.work_items")}
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={
projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""
}
/>
}
/>
</Breadcrumbs>
</div>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem> <Header.RightItem>
{projectId && issueId && ( {projectId && issueId && (

View file

@ -41,9 +41,8 @@ export const WorkspaceDraftHeader = observer(() => {
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink label={t("drafts")} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} /> <BreadcrumbLink label={t("drafts")} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />
} }
/> />

View file

@ -28,9 +28,8 @@ export const WorkspaceDashboardHeader = () => {
<Header.LeftItem> <Header.LeftItem>
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} /> <BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} />
} }
/> />

View file

@ -52,9 +52,8 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={breadcrumbLabel} label={breadcrumbLabel}
disableTooltip disableTooltip

View file

@ -66,10 +66,9 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <ProjectBreadcrumb workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`} href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Archives" label="Archives"
@ -78,9 +77,8 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
} }
/> />
{activeTabBreadcrumbDetail && ( {activeTabBreadcrumbDetail && (
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={activeTabBreadcrumbDetail.label} label={activeTabBreadcrumbDetail.label}
icon={<activeTabBreadcrumbDetail.icon className="h-4 w-4 text-custom-text-300" />} icon={<activeTabBreadcrumbDetail.icon className="h-4 w-4 text-custom-text-300" />}

View file

@ -36,10 +36,9 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs isLoading={loader === "init-loader"}> <Breadcrumbs isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <ProjectBreadcrumb workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`} href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Archives" label="Archives"
@ -47,9 +46,8 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
/> />
} }
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`} href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Work items" label="Work items"
@ -57,9 +55,8 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
/> />
} }
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={ label={
currentProjectDetails && issueDetails currentProjectDetails && issueDetails

View file

@ -2,7 +2,6 @@
import { useCallback, useRef, 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 { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// icons // icons
import { PanelRight } from "lucide-react"; import { PanelRight } from "lucide-react";
@ -13,8 +12,10 @@ import {
EIssuesStoreType, EIssuesStoreType,
EUserPermissions, EUserPermissions,
EUserPermissionsLevel, EUserPermissionsLevel,
EProjectFeatureKey,
ISSUE_DISPLAY_FILTERS_BY_PAGE, ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants"; } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { import {
ICustomSearchSelectOption, ICustomSearchSelectOption,
@ -22,11 +23,11 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
IIssueFilterOptions, IIssueFilterOptions,
} from "@plane/types"; } from "@plane/types";
import { Breadcrumbs, Button, ContrastIcon, CustomSearchSelect, Header, Tooltip } from "@plane/ui"; import { Breadcrumbs, Button, ContrastIcon, BreadcrumbNavigationSearchDropdown, Header, Tooltip } from "@plane/ui";
import { cn, isIssueFilterActive } from "@plane/utils"; import { cn, isIssueFilterActive } from "@plane/utils";
// components // components
import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { SwitcherLabel } from "@/components/common";
import { CycleQuickActions } from "@/components/cycles"; import { CycleQuickActions } from "@/components/cycles";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// hooks // hooks
@ -43,9 +44,8 @@ import {
} from "@/hooks/store"; } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web imports // plane web imports
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
export const CycleIssuesHeader: React.FC = observer(() => { export const CycleIssuesHeader: React.FC = observer(() => {
// refs // refs
@ -166,63 +166,44 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<Breadcrumbs.BreadcrumbItem <CommonProjectBreadcrumbs
type="text" workspaceSlug={workspaceSlug?.toString()}
link={ projectId={projectId?.toString()}
<span> featureKey={EProjectFeatureKey.CYCLES}
<span className="hidden md:block">
<ProjectBreadcrumb />
</span>
<Link
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
className="block pl-2 text-custom-text-300 md:hidden"
>
...
</Link>
</span>
}
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text"
link={
<BreadcrumbLink
label={t("common.cycles")}
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="component"
component={ component={
<CustomSearchSelect <BreadcrumbNavigationSearchDropdown
options={switcherOptions} selectedItem={cycleId}
value={cycleId} navigationItems={switcherOptions}
onChange={(value: string) => { onChange={(value: string) => {
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${value}`); router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${value}`);
}} }}
label={ title={cycleDetails?.name}
<div className="flex items-center gap-1"> icon={
<SwitcherLabel name={cycleDetails?.name} LabelIcon={ContrastIcon} /> <Breadcrumbs.Icon>
{workItemsCount && workItemsCount > 0 ? ( <ContrastIcon className="size-4 flex-shrink-0 text-custom-text-300" />
<Tooltip </Breadcrumbs.Icon>
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>
} }
isLast
/> />
} }
isLast
/> />
</Breadcrumbs> </Breadcrumbs>
{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> </div>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem className="items-center"> <Header.RightItem className="items-center">

View file

@ -2,23 +2,25 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui // ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common";
import { CyclesViewHeader } from "@/components/cycles"; import { CyclesViewHeader } from "@/components/cycles";
// hooks // hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
// constants // constants
export const CyclesListHeader: FC = observer(() => { export const CyclesListHeader: FC = observer(() => {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug } = useParams();
// store hooks // store hooks
const { toggleCreateCycleModal } = useCommandPalette(); const { toggleCreateCycleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
@ -35,15 +37,11 @@ export const CyclesListHeader: FC = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
<Breadcrumbs.BreadcrumbItem workspaceSlug={workspaceSlug?.toString()}
type="text" projectId={currentProjectDetails?.id ?? ""}
link={ featureKey={EProjectFeatureKey.CYCLES}
<BreadcrumbLink isLast
label={t("cycle.label", { count: 2 })}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/> />
</Breadcrumbs> </Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>

View file

@ -93,11 +93,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Breadcrumbs isLoading={loader === "init-loader"}> <Breadcrumbs isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label="Draft work items" label="Draft work items"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}

View file

@ -2,7 +2,6 @@
import { useCallback, useRef, 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 { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// icons // icons
import { PanelRight } from "lucide-react"; import { PanelRight } from "lucide-react";
@ -14,6 +13,7 @@ import {
ISSUE_DISPLAY_FILTERS_BY_PAGE, ISSUE_DISPLAY_FILTERS_BY_PAGE,
EUserPermissions, EUserPermissions,
EUserPermissionsLevel, EUserPermissionsLevel,
EProjectFeatureKey,
} from "@plane/constants"; } from "@plane/constants";
import { import {
ICustomSearchSelectOption, ICustomSearchSelectOption,
@ -21,11 +21,11 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
IIssueFilterOptions, IIssueFilterOptions,
} from "@plane/types"; } from "@plane/types";
import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; import { Breadcrumbs, Button, DiceIcon, Header, BreadcrumbNavigationSearchDropdown, Tooltip } from "@plane/ui";
import { cn, isIssueFilterActive } from "@plane/utils"; import { cn, isIssueFilterActive } from "@plane/utils";
// components // components
import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { SwitcherLabel } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// helpers // helpers
import { ModuleQuickActions } from "@/components/modules"; import { ModuleQuickActions } from "@/components/modules";
@ -46,7 +46,7 @@ import { useIssuesActions } from "@/hooks/use-issues-actions";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
export const ModuleIssuesHeader: React.FC = observer(() => { export const ModuleIssuesHeader: React.FC = observer(() => {
// refs // refs
@ -160,64 +160,42 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
/> />
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <div className="flex items-center gap-2 flex-grow">
<Breadcrumbs.BreadcrumbItem <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
type="text" <CommonProjectBreadcrumbs
link={ workspaceSlug={workspaceSlug?.toString() ?? ""}
<span> projectId={projectId?.toString() ?? ""}
<span className="hidden md:block"> featureKey={EProjectFeatureKey.MODULES}
<ProjectBreadcrumb /> />
</span> <Breadcrumbs.Item
<Link component={
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} <BreadcrumbNavigationSearchDropdown
className="block pl-2 text-custom-text-300 md:hidden" selectedItem={moduleId?.toString() ?? ""}
> navigationItems={switcherOptions}
... onChange={(value: string) => {
</Link> router.push(`/${workspaceSlug}/projects/${projectId}/modules/${value}`);
}}
title={moduleDetails?.name}
icon={<DiceIcon className="size-3.5 flex-shrink-0 text-custom-text-300" />}
isLast
/>
}
/>
</Breadcrumbs>
{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> </span>
} </Tooltip>
/> ) : null}
<Breadcrumbs.BreadcrumbItem </div>
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/modules`}
label="Modules"
icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<CustomSearchSelect
options={switcherOptions}
label={
<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>
}
value={moduleId}
onChange={(value: string) => {
router.push(`/${workspaceSlug}/projects/${projectId}/modules/${value}`);
}}
/>
}
/>
</Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem className="items-center"> <Header.RightItem className="items-center">
<div className="hidden gap-2 md:flex"> <div className="hidden gap-2 md:flex">

View file

@ -1,24 +1,25 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports // plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common";
import { ModuleViewHeader } from "@/components/modules"; import { ModuleViewHeader } from "@/components/modules";
// hooks // hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
// constants // constants
export const ModulesListHeader: React.FC = observer(() => { export const ModulesListHeader: React.FC = observer(() => {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks // store hooks
const { toggleCreateModuleModal } = useCommandPalette(); const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
@ -39,12 +40,11 @@ export const ModulesListHeader: React.FC = observer(() => {
<Header.LeftItem> <Header.LeftItem>
<div> <div>
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
<Breadcrumbs.BreadcrumbItem workspaceSlug={workspaceSlug?.toString() ?? ""}
type="text" projectId={projectId?.toString() ?? ""}
link={ featureKey={EProjectFeatureKey.MODULES}
<BreadcrumbLink label={t("modules")} icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} /> isLast
}
/> />
</Breadcrumbs> </Breadcrumbs>
</div> </div>

View file

@ -2,20 +2,21 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { FileText } from "lucide-react"; import { FileText } from "lucide-react";
import { EProjectFeatureKey } from "@plane/constants";
// types // types
import { ICustomSearchSelectOption } from "@plane/types"; import { ICustomSearchSelectOption } from "@plane/types";
// ui // ui
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui"; import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
// components // components
import { getPageName } from "@plane/utils"; import { getPageName } from "@plane/utils";
import { BreadcrumbLink, PageAccessIcon, SwitcherLabel } from "@/components/common"; import { PageAccessIcon, SwitcherIcon, SwitcherLabel } from "@/components/common";
import { PageHeaderActions } from "@/components/pages/header/actions"; import { PageHeaderActions } from "@/components/pages/header/actions";
// helpers // helpers
// hooks // hooks
import { useProject } from "@/hooks/store"; import { useProject } from "@/hooks/store";
// plane web components // plane web components
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages"; import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
// plane web hooks // plane web hooks
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store"; import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
@ -31,7 +32,7 @@ export const PageDetailsHeader = observer(() => {
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, pageId, projectId } = useParams(); const { workspaceSlug, pageId, projectId } = useParams();
// store hooks // store hooks
const { currentProjectDetails, loader } = useProject(); const { loader } = useProject();
const { getPageById, getCurrentProjectPageIds } = usePageStore(storeType); const { getPageById, getCurrentProjectPageIds } = usePageStore(storeType);
const page = usePage({ const page = usePage({
pageId: pageId?.toString() ?? "", pageId: pageId?.toString() ?? "",
@ -64,45 +65,27 @@ export const PageDetailsHeader = observer(() => {
<Header.LeftItem> <Header.LeftItem>
<div> <div>
<Breadcrumbs isLoading={loader === "init-loader"}> <Breadcrumbs isLoading={loader === "init-loader"}>
<Breadcrumbs.BreadcrumbItem <CommonProjectBreadcrumbs
type="text" workspaceSlug={workspaceSlug?.toString()}
link={ projectId={projectId?.toString()}
<span> featureKey={EProjectFeatureKey.PAGES}
<span className="hidden md:block">
<ProjectBreadcrumb />
</span>
<span className="md:hidden">
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={"..."}
/>
</span>
</span>
}
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages`}
label="Pages"
icon={<FileText className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="component"
component={ component={
<CustomSearchSelect <BreadcrumbNavigationSearchDropdown
value={pageId} selectedItem={pageId?.toString() ?? ""}
options={switcherOptions} navigationItems={switcherOptions}
label={
<SwitcherLabel logo_props={page.logo_props} name={getPageName(page.name)} LabelIcon={FileText} />
}
onChange={(value: string) => { onChange={(value: string) => {
router.push(`/${workspaceSlug}/projects/${projectId}/pages/${value}`); router.push(`/${workspaceSlug}/projects/${projectId}/pages/${value}`);
}} }}
title={page?.name}
icon={
<Breadcrumbs.Icon>
<SwitcherIcon logo_props={page.logo_props} LabelIcon={FileText} size={16} />
</Breadcrumbs.Icon>
}
isLast
/> />
} }
/> />

View file

@ -3,19 +3,16 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { FileText } from "lucide-react";
// constants // constants
import { EPageAccess } from "@plane/constants"; import { EPageAccess, EProjectFeatureKey } from "@plane/constants";
// plane types // plane types
import { TPage } from "@plane/types"; import { TPage } from "@plane/types";
// plane ui // plane ui
import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui"; import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { BreadcrumbLink } from "@/components/common";
// hooks // hooks
import { useEventTracker, useProject } from "@/hooks/store"; import { useEventTracker, useProject } from "@/hooks/store";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
// plane web hooks // plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@ -58,15 +55,14 @@ export const PagesListHeader = observer(() => {
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<div> <Breadcrumbs isLoading={loader === "init-loader"}>
<Breadcrumbs isLoading={loader === "init-loader"}> <CommonProjectBreadcrumbs
<ProjectBreadcrumb /> workspaceSlug={workspaceSlug?.toString() ?? ""}
<Breadcrumbs.BreadcrumbItem projectId={currentProjectDetails?.id?.toString() ?? ""}
type="text" featureKey={EProjectFeatureKey.PAGES}
link={<BreadcrumbLink label="Pages" icon={<FileText className="h-4 w-4 text-custom-text-300" />} />} isLast
/> />
</Breadcrumbs> </Breadcrumbs>
</div>
</Header.LeftItem> </Header.LeftItem>
{canCurrentUserCreatePage ? ( {canCurrentUserCreatePage ? (
<Header.RightItem> <Header.RightItem>

View file

@ -13,6 +13,7 @@ import {
EViewAccess, EViewAccess,
EUserPermissions, EUserPermissions,
EUserPermissionsLevel, EUserPermissionsLevel,
EProjectFeatureKey,
} from "@plane/constants"; } from "@plane/constants";
// types // types
import { import {
@ -22,10 +23,10 @@ import {
IIssueFilterOptions, IIssueFilterOptions,
} from "@plane/types"; } from "@plane/types";
// ui // ui
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; import { Breadcrumbs, Button, Tooltip, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
// components // components
import { isIssueFilterActive } from "@plane/utils"; import { isIssueFilterActive } from "@plane/utils";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { SwitcherIcon, 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";
@ -44,7 +45,7 @@ import {
} from "@/hooks/store"; } from "@/hooks/store";
// plane web // plane web
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
// refs // refs
@ -164,27 +165,27 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs isLoading={loader === "init-loader"}> <Breadcrumbs isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
<Breadcrumbs.BreadcrumbItem workspaceSlug={workspaceSlug?.toString() ?? ""}
type="text" projectId={projectId?.toString() ?? ""}
link={ featureKey={EProjectFeatureKey.VIEWS}
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/views`}
label="Views"
icon={<Layers className="h-4 w-4 text-custom-text-300" />}
/>
}
/> />
<Breadcrumbs.BreadcrumbItem
type="component" <Breadcrumbs.Item
component={ component={
<CustomSearchSelect <BreadcrumbNavigationSearchDropdown
options={switcherOptions} selectedItem={viewId?.toString() ?? ""}
value={viewId} navigationItems={switcherOptions}
label={<SwitcherLabel logo_props={viewDetails.logo_props} name={viewDetails.name} LabelIcon={Layers} />}
onChange={(value: string) => { onChange={(value: string) => {
router.push(`/${workspaceSlug}/projects/${projectId}/views/${value}`); router.push(`/${workspaceSlug}/projects/${projectId}/views/${value}`);
}} }}
title={viewDetails?.name}
icon={
<Breadcrumbs.Icon>
<SwitcherIcon logo_props={viewDetails.logo_props} LabelIcon={Layers} size={16} />
</Breadcrumbs.Icon>
}
isLast
/> />
} }
/> />

View file

@ -1,18 +1,19 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Layers } from "lucide-react"; import { useParams } from "next/navigation";
// ui // ui
import { EProjectFeatureKey } from "@plane/constants";
import { Breadcrumbs, Button, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common";
import { ViewListHeader } from "@/components/views"; import { ViewListHeader } from "@/components/views";
// hooks // hooks
import { useCommandPalette, useProject } from "@/hooks/store"; import { useCommandPalette, useProject } from "@/hooks/store";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
export const ProjectViewsHeader = observer(() => { export const ProjectViewsHeader = observer(() => {
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks // store hooks
const { toggleCreateViewModal } = useCommandPalette(); const { toggleCreateViewModal } = useCommandPalette();
const { loader } = useProject(); const { loader } = useProject();
@ -22,10 +23,11 @@ export const ProjectViewsHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs isLoading={loader === "init-loader"}> <Breadcrumbs isLoading={loader === "init-loader"}>
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
<Breadcrumbs.BreadcrumbItem workspaceSlug={workspaceSlug?.toString() ?? ""}
type="text" projectId={projectId?.toString() ?? ""}
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />} featureKey={EProjectFeatureKey.VIEWS}
isLast
/> />
</Breadcrumbs> </Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>

View file

@ -25,9 +25,8 @@ export const WorkspaceStickyHeader = observer(() => {
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={`Stickies`} label={`Stickies`}
icon={<RecentStickyIcon className="size-5 rotate-90 text-custom-text-200" />} icon={<RecentStickyIcon className="size-5 rotate-90 text-custom-text-200" />}

View file

@ -8,7 +8,6 @@ import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { AllIssueLayoutRoot, GlobalViewsAppliedFiltersRoot } from "@/components/issues"; import { AllIssueLayoutRoot, GlobalViewsAppliedFiltersRoot } from "@/components/issues";
import { GlobalViewsHeader } from "@/components/workspace";
// constants // constants
// hooks // hooks
import { useWorkspace } from "@/hooks/store"; import { useWorkspace } from "@/hooks/store";
@ -32,7 +31,6 @@ const GlobalViewIssuesPage = observer(() => {
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />
<div className="h-full overflow-hidden bg-custom-background-100"> <div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300"> <div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
{globalViewId && ( {globalViewId && (
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} isLoading={isLoading} /> <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} isLoading={isLoading} />
)} )}

View file

@ -5,32 +5,49 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Layers } from "lucide-react"; import { Layers } from "lucide-react";
// plane constants // plane constants
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import {
DEFAULT_GLOBAL_VIEWS_LIST,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EIssueLayoutTypes
} from "@plane/constants";
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, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
// components // components
import { isIssueFilterActive } from "@plane/utils"; import { isIssueFilterActive } from "@plane/utils";
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; import {
CreateUpdateWorkspaceViewModal,
WorkspaceViewQuickActions,
DefaultWorkspaceViewQuickActions,
} from "@/components/workspace";
// helpers // helpers
// hooks // hooks
import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper"; import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper";
export const GlobalIssuesHeader = observer(() => { export const GlobalIssuesHeader = observer(() => {
// states // states
const [createViewModal, setCreateViewModal] = useState(false); const [createViewModal, setCreateViewModal] = useState(false);
// router // router
const router = useAppRouter();
const { workspaceSlug, globalViewId } = useParams(); const { workspaceSlug, globalViewId } = useParams();
// store hooks // store hooks
const { const {
issuesFilter: { filters, updateFilters }, issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { getViewDetailsById } = useGlobalView(); const { getViewDetailsById, currentWorkspaceViews } = useGlobalView();
const { workspaceLabels } = useLabel(); const { workspaceLabels } = useLabel();
const { const {
workspace: { workspaceMemberIds }, workspace: { workspaceMemberIds },
@ -113,6 +130,29 @@ export const GlobalIssuesHeader = observer(() => {
const isLocked = viewDetails?.is_locked; const isLocked = viewDetails?.is_locked;
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId);
const defaultViewDetails = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId);
const defaultOptions = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => ({
value: view.key,
query: view.key,
content: <SwitcherLabel name={t(view.i18n_label)} LabelIcon={Layers} />,
}));
const workspaceOptions = (currentWorkspaceViews || []).map((view) => {
const _view = getViewDetailsById(view);
if (!_view) return;
return {
value: _view.id,
query: _view.name,
content: <SwitcherLabel name={_view.name} LabelIcon={Layers} />,
};
});
const switcherOptions = [...defaultOptions, ...workspaceOptions].filter(
(option) => option !== undefined
) as ICustomSearchSelectOption[];
const currentLayoutFilters = useMemo(() => { const currentLayoutFilters = useMemo(() => {
const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET; const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET;
return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout]; return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout];
@ -124,9 +164,29 @@ export const GlobalIssuesHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={<BreadcrumbLink label={t("views")} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />} <BreadcrumbLink label={t("views")} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />
}
/>
<Breadcrumbs.Item
component={
<BreadcrumbNavigationSearchDropdown
selectedItem={globalViewId?.toString() || ""}
navigationItems={switcherOptions}
onChange={(value: string) => {
router.push(`/${workspaceSlug}/workspace-views/${value}`);
}}
title={viewDetails?.name ?? t(defaultViewDetails?.i18n_label ?? "")}
icon={
<Breadcrumbs.Icon>
<Layers className="size-4 flex-shrink-0 text-custom-text-300" />
</Breadcrumbs.Icon>
}
isLast
/>
}
isLast
/> />
</Breadcrumbs> </Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>
@ -171,6 +231,12 @@ export const GlobalIssuesHeader = observer(() => {
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}> <Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
{t("workspace_views.add_view")} {t("workspace_views.add_view")}
</Button> </Button>
<div className="hidden md:block">
{viewDetails && <WorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={viewDetails} />}
{isDefaultView && defaultViewDetails && (
<DefaultWorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={defaultViewDetails} />
)}
</div>
</Header.RightItem> </Header.RightItem>
</Header> </Header>
</> </>

View file

@ -0,0 +1,32 @@
"use client";
import { FC } from "react";
// plane imports
import { EProjectFeatureKey } from "@plane/constants";
// local components
import { ProjectFeatureBreadcrumb } from "./project-feature";
import { ProjectBreadcrumb } from "./project";
type TCommonProjectBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
featureKey?: EProjectFeatureKey;
isLast?: boolean;
};
export const CommonProjectBreadcrumbs: FC<TCommonProjectBreadcrumbProps> = (props) => {
const { workspaceSlug, projectId, featureKey, isLast = false } = props;
return (
<>
<ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />
{featureKey && (
<ProjectFeatureBreadcrumb
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
featureKey={featureKey}
isLast={isLast}
/>
)}
</>
);
};

View file

@ -1 +1,3 @@
export * from "./common";
export * from "./project-feature";
export * from "./project"; export * from "./project";

View file

@ -0,0 +1,69 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// ui
import { EProjectFeatureKey } from "@plane/constants";
import { BreadcrumbNavigationDropdown, Breadcrumbs, ISvgIcons } from "@plane/ui";
// components
import { SwitcherLabel } from "@/components/common";
import { TNavigationItem } from "@/components/workspace";
// hooks
import { useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// local components
import { getProjectFeatureNavigation } from "../projects/navigation";
type TProjectFeatureBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
featureKey: EProjectFeatureKey;
isLast?: boolean;
additionalNavigationItems?: TNavigationItem[];
};
export const ProjectFeatureBreadcrumb = observer((props: TProjectFeatureBreadcrumbProps) => {
const { workspaceSlug, projectId, featureKey, isLast = false, additionalNavigationItems } = props;
// router
const router = useAppRouter();
// store hooks
const { getPartialProjectById } = useProject();
// derived values
const project = getPartialProjectById(projectId);
if (!project) return null;
const navigationItems = getProjectFeatureNavigation(workspaceSlug, projectId, project);
// if additional navigation items are provided, add them to the navigation items
const allNavigationItems = [...(additionalNavigationItems || []), ...navigationItems];
return (
<>
<Breadcrumbs.Item
component={
<BreadcrumbNavigationDropdown
selectedItemKey={featureKey}
navigationItems={allNavigationItems
.filter((item) => item.shouldRender)
.map((item) => ({
key: item.key,
title: item.name,
customContent: <SwitcherLabel name={item.name} LabelIcon={item.icon as FC<ISvgIcons>} />,
action: () => router.push(item.href),
icon: item.icon as FC<ISvgIcons>,
}))}
handleOnClick={() => {
router.push(
`/${workspaceSlug}/projects/${projectId}/${featureKey === EProjectFeatureKey.WORK_ITEMS ? "issues" : featureKey}/`
);
}}
isLast={isLast}
/>
}
showSeparator={false}
isLast={isLast}
/>
</>
);
});

View file

@ -2,38 +2,73 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Briefcase } from "lucide-react"; import { Briefcase } from "lucide-react";
// ui // plane imports
import { Breadcrumbs, Logo } from "@plane/ui"; import { ICustomSearchSelectOption } from "@plane/types";
import { BreadcrumbNavigationSearchDropdown, Breadcrumbs, Logo } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { SwitcherLabel } from "@/components/common";
// hooks // hooks
import { useProject } from "@/hooks/store"; import { useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { TProject } from "@/plane-web/types";
export const ProjectBreadcrumb = observer(() => { type TProjectBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
handleOnClick?: () => void;
};
export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => {
const { workspaceSlug, projectId, handleOnClick } = props;
// router
const router = useAppRouter();
// store hooks // store hooks
const { currentProjectDetails } = useProject(); const { joinedProjectIds, getPartialProjectById } = useProject();
const currentProjectDetails = getPartialProjectById(projectId);
// store hooks
if (!currentProjectDetails) return null;
// derived values
const switcherOptions = joinedProjectIds
.map((projectId) => {
const project = getPartialProjectById(projectId);
return {
value: projectId,
query: project?.name,
content: <SwitcherLabel name={project?.name} logo_props={project?.logo_props} LabelIcon={Briefcase} />,
};
})
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
// helpers
const renderIcon = (projectDetails: TProject) => (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails.logo_props} size={14} />
</span>
);
return ( return (
<Breadcrumbs.BreadcrumbItem <>
type="text" <Breadcrumbs.Item
link={ component={
<BreadcrumbLink <BreadcrumbNavigationSearchDropdown
label={currentProjectDetails?.name ?? "Project"} selectedItem={currentProjectDetails.id}
icon={ navigationItems={switcherOptions}
currentProjectDetails ? ( onChange={(value: string) => {
currentProjectDetails && ( router.push(`/${workspaceSlug}/projects/${value}/issues`);
<span className="grid place-items-center flex-shrink-0 h-4 w-4"> }}
<Logo logo={currentProjectDetails?.logo_props} size={16} /> title={currentProjectDetails?.name}
</span> icon={renderIcon(currentProjectDetails)}
) handleOnClick={() => {
) : ( if (handleOnClick) handleOnClick();
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
<Briefcase className="h-4 w-4" /> }}
</span> />
) }
} showSeparator={false}
/> />
} </>
/>
); );
}); });

View file

@ -4,13 +4,20 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// icons // icons
import { Circle, ExternalLink } from "lucide-react"; import { Circle, ExternalLink } from "lucide-react";
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel, SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants"; import {
EIssuesStoreType,
EProjectFeatureKey,
EUserPermissions,
EUserPermissionsLevel,
SPACE_BASE_PATH,
SPACE_BASE_URL,
} from "@plane/constants";
// plane constants // plane constants
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui"; import { Breadcrumbs, Button, Tooltip, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink, CountChip } from "@/components/common"; import { CountChip } from "@/components/common";
// constants // constants
import HeaderFilters from "@/components/issues/filters"; import HeaderFilters from "@/components/issues/filters";
// helpers // helpers
@ -20,7 +27,7 @@ import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "../breadcrumbs/common";
export const IssuesHeader = observer(() => { export const IssuesHeader = observer(() => {
// router // router
@ -52,18 +59,13 @@ export const IssuesHeader = observer(() => {
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5 flex-grow">
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"}> <Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"} className="flex-grow-0">
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
workspaceSlug={workspaceSlug?.toString()}
<Breadcrumbs.BreadcrumbItem projectId={projectId?.toString()}
type="text" featureKey={EProjectFeatureKey.WORK_ITEMS}
link={ isLast
<BreadcrumbLink
label={t("issue.label", { count: 2 })} // count is for pluralization
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/> />
</Breadcrumbs> </Breadcrumbs>
{issuesCount && issuesCount > 0 ? ( {issuesCount && issuesCount > 0 ? (

View file

@ -0,0 +1,77 @@
import { FileText, Layers } from "lucide-react";
import { EUserPermissions, EProjectFeatureKey } from "@plane/constants";
import { ContrastIcon, DiceIcon, Intake, LayersIcon } from "@plane/ui";
import { TNavigationItem } from "@/components/workspace";
export const getProjectFeatureNavigation = (
workspaceSlug: string,
projectId: string,
project: {
cycle_view: boolean;
module_view: boolean;
issue_views_view: boolean;
page_view: boolean;
inbox_view: boolean;
}
): TNavigationItem[] => [
{
i18n_key: "sidebar.work_items",
key: EProjectFeatureKey.WORK_ITEMS,
name: "Work items",
href: `/${workspaceSlug}/projects/${projectId}/issues`,
icon: LayersIcon,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
shouldRender: true,
sortOrder: 1,
},
{
i18n_key: "sidebar.cycles",
key: EProjectFeatureKey.CYCLES,
name: "Cycles",
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
icon: ContrastIcon,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
shouldRender: project.cycle_view,
sortOrder: 2,
},
{
i18n_key: "sidebar.modules",
key: EProjectFeatureKey.MODULES,
name: "Modules",
href: `/${workspaceSlug}/projects/${projectId}/modules`,
icon: DiceIcon,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
shouldRender: project.module_view,
sortOrder: 3,
},
{
i18n_key: "sidebar.views",
key: EProjectFeatureKey.VIEWS,
name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`,
icon: Layers,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
shouldRender: project.issue_views_view,
sortOrder: 4,
},
{
i18n_key: "sidebar.pages",
key: EProjectFeatureKey.PAGES,
name: "Pages",
href: `/${workspaceSlug}/projects/${projectId}/pages`,
icon: FileText,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
shouldRender: project.page_view,
sortOrder: 5,
},
{
i18n_key: "sidebar.intake",
key: EProjectFeatureKey.INTAKE,
name: "Intake",
href: `/${workspaceSlug}/projects/${projectId}/intake`,
icon: Intake,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
shouldRender: project.inbox_view,
sortOrder: 6,
},
];

View file

@ -0,0 +1 @@
export * from "./helper";

View file

@ -5,16 +5,15 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { RefreshCcw } from "lucide-react"; import { RefreshCcw } from "lucide-react";
// ui // ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, Intake, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common";
import { InboxIssueCreateModalRoot } from "@/components/inbox"; import { InboxIssueCreateModalRoot } from "@/components/inbox";
// hooks // hooks
import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store"; import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
export const ProjectInboxHeader: FC = observer(() => { export const ProjectInboxHeader: FC = observer(() => {
// states // states
@ -37,13 +36,13 @@ export const ProjectInboxHeader: FC = observer(() => {
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 flex-grow border">
<Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}> <Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
<ProjectBreadcrumb /> <CommonProjectBreadcrumbs
workspaceSlug={workspaceSlug?.toString() ?? ""}
<Breadcrumbs.BreadcrumbItem projectId={projectId?.toString() ?? ""}
type="text" featureKey={EProjectFeatureKey.INTAKE}
link={<BreadcrumbLink label={t("intake")} icon={<Intake className="h-4 w-4 text-custom-text-300" />} />} isLast
/> />
</Breadcrumbs> </Breadcrumbs>

View file

@ -1,44 +1,75 @@
"use client"; "use client";
import { ReactNode } from "react"; import React, { ReactNode, useMemo, FC } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { Tooltip } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = { type Props = {
label?: string | ReactNode; label?: string;
href?: string; href?: string;
icon?: React.ReactNode | undefined; icon?: React.ReactNode;
disableTooltip?: boolean; disableTooltip?: boolean;
isLast?: boolean;
}; };
export const BreadcrumbLink: React.FC<Props> = (props) => { const IconWrapper = React.memo(({ icon }: { icon: React.ReactNode }) => (
const { href, label, icon, disableTooltip = false } = props; <div className="flex size-4 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
const { isMobile } = usePlatformOS(); ));
IconWrapper.displayName = "IconWrapper";
const LabelWrapper = React.memo(({ label }: { label: ReactNode }) => (
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
));
LabelWrapper.displayName = "LabelWrapper";
const BreadcrumbContent = React.memo(({ icon, label }: { icon?: React.ReactNode; label?: ReactNode }) => {
if (!icon && !label) return null;
return ( return (
<Tooltip tooltipContent={label} position="bottom" isMobile={isMobile} disabled={disableTooltip}> <>
<li className="flex items-center space-x-2" tabIndex={-1}> {icon && <IconWrapper icon={icon} />}
<div className="flex flex-wrap items-center gap-2.5"> {label && <LabelWrapper label={label} />}
{href ? ( </>
<Link
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
href={href}
>
{icon && (
<div className="flex h-5 w-5 items-center justify-start overflow-hidden !text-[1rem]">{icon}</div>
)}
{label && (
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
)}
</Link>
) : (
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
{icon && <div className="flex h-5 w-5 items-center justify-start overflow-hidden">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</div>
)}
</div>
</li>
</Tooltip>
); );
}; });
BreadcrumbContent.displayName = "BreadcrumbContent";
const ItemWrapper = React.memo(({ children, ...props }: React.ComponentProps<typeof Breadcrumbs.ItemWrapper>) => (
<Breadcrumbs.ItemWrapper {...props}>{children}</Breadcrumbs.ItemWrapper>
));
ItemWrapper.displayName = "ItemWrapper";
export const BreadcrumbLink: FC<Props> = observer((props) => {
const { href, label, icon, disableTooltip = false, isLast = false } = props;
const { isMobile } = usePlatformOS();
const itemWrapperProps = useMemo(
() => ({
label: label?.toString(),
disableTooltip: isMobile || disableTooltip,
type: (href && href !== "" ? "link" : "text") as "link" | "text",
isLast,
}),
[href, label, isMobile, disableTooltip, isLast]
);
const content = useMemo(() => <BreadcrumbContent icon={icon} label={label} />, [icon, label]);
if (href) {
return (
<Link href={href}>
<ItemWrapper {...itemWrapperProps}>{content}</ItemWrapper>
</Link>
);
}
return <ItemWrapper {...itemWrapperProps}>{content}</ItemWrapper>;
});
BreadcrumbLink.displayName = "BreadcrumbLink";

View file

@ -2,6 +2,32 @@ import { FC } from "react";
import { TLogoProps } from "@plane/types"; import { TLogoProps } from "@plane/types";
import { ISvgIcons, Logo } from "@plane/ui"; import { ISvgIcons, Logo } from "@plane/ui";
import { getFileURL, truncateText } from "@plane/utils"; import { getFileURL, truncateText } from "@plane/utils";
type TSwitcherIconProps = {
logo_props?: TLogoProps;
logo_url?: string;
LabelIcon: FC<ISvgIcons>;
size?: number;
};
export const SwitcherIcon: FC<TSwitcherIconProps> = ({ logo_props, logo_url, LabelIcon, size = 12 }) => {
if (logo_props?.in_use) {
return <Logo logo={logo_props} size={size} type="lucide" />;
}
if (logo_url) {
return (
<img
src={getFileURL(logo_url)}
alt="logo"
className="rounded-sm object-cover"
style={{ height: size, width: size }}
/>
);
}
return <LabelIcon height={size} width={size} />;
};
type TSwitcherLabelProps = { type TSwitcherLabelProps = {
logo_props?: TLogoProps; logo_props?: TLogoProps;
logo_url?: string; logo_url?: string;
@ -13,13 +39,7 @@ export const SwitcherLabel: FC<TSwitcherLabelProps> = (props) => {
const { logo_props, name, LabelIcon, logo_url } = props; const { logo_props, name, LabelIcon, logo_url } = props;
return ( return (
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center gap-1 text-custom-text-200">
{logo_props?.in_use ? ( <SwitcherIcon logo_props={logo_props} logo_url={logo_url} LabelIcon={LabelIcon} />
<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)} {truncateText(name ?? "", 40)}
</div> </div>
); );

View file

@ -37,16 +37,15 @@ export const ProjectsBaseHeader = observer(() => {
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={t("workspace_projects.label", { count: 2 })} label={t("workspace_projects.label", { count: 2 })}
icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} icon={<Briefcase className="h-4 w-4 text-custom-text-300" />}
/> />
} }
/> />
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />} {isArchived && <Breadcrumbs.Item component={<BreadcrumbLink label="Archived" />} />}
</Breadcrumbs> </Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem> <Header.RightItem>

View file

@ -26,9 +26,8 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
</div> </div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.Item
type="text" component={
link={
<BreadcrumbLink <BreadcrumbLink
label={t("notification.label")} label={t("notification.label")}
icon={<Inbox className="h-4 w-4 text-custom-text-300" />} icon={<Inbox className="h-4 w-4 text-custom-text-300" />}

View file

@ -1,19 +1,16 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link";
import { ExternalLink, LinkIcon } from "lucide-react"; import { ExternalLink, LinkIcon } from "lucide-react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { TStaticViewTypes } from "@plane/types"; import { TStaticViewTypes } from "@plane/types";
import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils"; import { copyUrlToClipboard, cn } from "@plane/utils";
// helpers // helpers
type Props = { type Props = {
parentRef: React.RefObject<HTMLElement>;
workspaceSlug: string; workspaceSlug: string;
globalViewId: string | undefined;
view: { view: {
key: TStaticViewTypes; key: TStaticViewTypes;
i18n_label: string; i18n_label: string;
@ -21,7 +18,7 @@ type Props = {
}; };
export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props) => { export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
const { parentRef, globalViewId, view, workspaceSlug } = props; const { workspaceSlug, view } = props;
const { t } = useTranslation(); const { t } = useTranslation();
@ -53,43 +50,11 @@ export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props
return ( return (
<> <>
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<CustomMenu <CustomMenu
customButton={ ellipsis
<>
{view.key === globalViewId ? (
<span
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
view.key === globalViewId
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
}`}
>
{t(view.i18n_label)}
</span>
) : (
<Link
key={view.key}
id={`global-view-${view.key}`}
href={`/${workspaceSlug}/workspace-views/${view.key}`}
>
<span
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
view.key === globalViewId
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
}`}
>
{t(view.i18n_label)}
</span>
</Link>
)}
</>
}
placement="bottom-end" placement="bottom-end"
menuItemsClassName="z-20"
closeOnSelect closeOnSelect
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded"
> >
{MENU_ITEMS.map((item) => { {MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null; if (item.shouldRender === false) return null;

View file

@ -37,13 +37,7 @@ const ViewTab = observer((props: { viewId: string }) => {
return ( return (
<div ref={parentRef} className="relative"> <div ref={parentRef} className="relative">
<WorkspaceViewQuickActions <WorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={view} />
parentRef={parentRef}
view={view}
viewId={viewId}
globalViewId={globalViewId?.toString()}
workspaceSlug={workspaceSlug?.toString()}
/>
</div> </div>
); );
}); });
@ -63,12 +57,7 @@ const DefaultViewTab = (props: {
if (!workspaceSlug || !globalViewId) return null; if (!workspaceSlug || !globalViewId) return null;
return ( return (
<div key={tab.key} ref={parentRef} className="relative"> <div key={tab.key} ref={parentRef} className="relative">
<DefaultWorkspaceViewQuickActions <DefaultWorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={tab} />
parentRef={parentRef}
globalViewId={globalViewId?.toString()}
workspaceSlug={workspaceSlug?.toString()}
view={tab}
/>
</div> </div>
); );
}; };

View file

@ -2,13 +2,12 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
import { ExternalLink, LinkIcon, Pencil, Trash2, Lock } from "lucide-react";
// types // types
import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IWorkspaceView } from "@plane/types"; import { IWorkspaceView } from "@plane/types";
import { ContextMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils"; import { copyUrlToClipboard, cn } from "@plane/utils";
// components // components
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
@ -18,15 +17,12 @@ import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/compone
import { useUser, useUserPermissions } from "@/hooks/store"; import { useUser, useUserPermissions } from "@/hooks/store";
type Props = { type Props = {
parentRef: React.RefObject<HTMLElement>;
workspaceSlug: string; workspaceSlug: string;
globalViewId: string;
viewId: string;
view: IWorkspaceView; view: IWorkspaceView;
}; };
export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => { export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
const { parentRef, view, globalViewId, viewId, workspaceSlug } = props; const { workspaceSlug, view } = props;
// states // states
const [updateViewModal, setUpdateViewModal] = useState(false); const [updateViewModal, setUpdateViewModal] = useState(false);
const [deleteViewModal, setDeleteViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false);
@ -78,42 +74,53 @@ export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
}, },
]; ];
const isSelected = viewId === globalViewId;
const isPrivateView = view.access === EViewAccess.PRIVATE;
let customButton = (
<div
className={`flex gap-1 items-center flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
isSelected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
} ${isPrivateView ? "pr-2" : ""}`}
>
<span className={`flex min-w-min flex-shrink-0 whitespace-nowrap text-sm font-medium outline-none`}>
{view.name}
</span>
{isPrivateView && (
<Lock className={`${isSelected ? "text-custom-primary-100" : "text-custom-text-400"} h-4 w-4`} />
)}
</div>
);
if (!isSelected) {
customButton = (
<Link key={viewId} id={`global-view-${viewId}`} href={`/${workspaceSlug}/workspace-views/${viewId}`}>
{customButton}
</Link>
);
}
return ( return (
<> <>
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} /> <CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
<DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} /> <DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} /> <CustomMenu
ellipsis
{customButton} placement="bottom-end"
closeOnSelect
buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded"
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</> </>
); );
}); });