[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:
parent
64fd0b2830
commit
2b7a17b484
44 changed files with 1251 additions and 581 deletions
|
|
@ -67,9 +67,8 @@ export const InstanceHeader: FC = observer(() => {
|
|||
{breadcrumbItems.length >= 0 && (
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
|
|
@ -80,10 +79,9 @@ export const InstanceHeader: FC = observer(() => {
|
|||
{breadcrumbItems.map(
|
||||
(item) =>
|
||||
item.title && (
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
<Breadcrumbs.Item
|
||||
key={item.title}
|
||||
type="text"
|
||||
link={<BreadcrumbLink href={item.href} label={item.title} />}
|
||||
component={<BreadcrumbLink href={item.href} label={item.title} />}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -149,3 +149,12 @@ export const DEFAULT_PROJECT_FORM_VALUES: Partial<IProject> = {
|
|||
network: 2,
|
||||
project_lead: null,
|
||||
};
|
||||
|
||||
export enum EProjectFeatureKey {
|
||||
WORK_ITEMS = "work_items",
|
||||
CYCLES = "cycles",
|
||||
MODULES = "modules",
|
||||
VIEWS = "views",
|
||||
PAGES = "pages",
|
||||
INTAKE = "intake",
|
||||
}
|
||||
|
|
|
|||
233
packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx
Normal file
233
packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx
Normal 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
|
||||
/>,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -1,13 +1,25 @@
|
|||
import * as React from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../helpers";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onBack?: () => void;
|
||||
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);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -22,35 +34,31 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
|
|||
|
||||
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 (
|
||||
<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 && (
|
||||
<>
|
||||
{childrenArray.map((child, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && !isSmallScreen && (
|
||||
<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" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? "hidden sm:flex" : "flex"}`}>
|
||||
{isLoading ? BreadcrumbItemLoader : child}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{childrenArray.map((child, index) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<BreadcrumbItemLoader />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (React.isValidElement<BreadcrumbItemProps>(child)) {
|
||||
return React.cloneElement(child, {
|
||||
isLast: index === childrenArray.length - 1,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isSmallScreen && childrenArray.length > 1 && (
|
||||
<>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex items-center gap-2.5 p-1">
|
||||
{onBack && (
|
||||
<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" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{isLoading ? BreadcrumbItemLoader : childrenArray[childrenArray.length - 1]}
|
||||
<div className="flex items-center gap-2.5 p-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>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -68,17 +84,107 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps)
|
|||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type?: "text" | "component";
|
||||
// breadcrumb item
|
||||
type BreadcrumbItemProps = {
|
||||
component?: React.ReactNode;
|
||||
link?: JSX.Element;
|
||||
showSeparator?: boolean;
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
const BreadcrumbItem: React.FC<Props> = (props) => {
|
||||
const { type = "text", component, link } = props;
|
||||
return <>{type !== "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = (props) => {
|
||||
const { component, showSeparator = true, isLast = false } = props;
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./breadcrumbs";
|
||||
export * from "./navigation-dropdown";
|
||||
export * from "./navigation-search-dropdown";
|
||||
|
|
|
|||
|
|
@ -1,42 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
||||
import { cn } from "../../helpers";
|
||||
// ui
|
||||
import { CustomMenu, TContextMenuItem } from "../dropdowns";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
|
||||
type TBreadcrumbNavigationDropdownProps = {
|
||||
selectedItemKey: string;
|
||||
navigationItems: TContextMenuItem[];
|
||||
navigationDisabled?: boolean;
|
||||
handleOnClick?: () => void;
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
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
|
||||
const selectedItem = navigationItems.find((item) => item.key === selectedItemKey);
|
||||
const selectedItemIcon = selectedItem?.icon ? (
|
||||
<selectedItem.icon className={cn("size-3.5", selectedItem.iconClassName)} />
|
||||
<selectedItem.icon className={cn("size-4", selectedItem.iconClassName)} />
|
||||
) : undefined;
|
||||
|
||||
// if no selected item, return null
|
||||
if (!selectedItem) return null;
|
||||
|
||||
const NavigationButton = ({ className }: { className?: string }) => (
|
||||
<li
|
||||
const NavigationButton = () => (
|
||||
<Tooltip tooltipContent={selectedItem.title} position="bottom" disabled={isOpen}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
if (!isLast) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleOnClick?.();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center cursor-default text-sm font-medium text-custom-text-200 group-hover:text-custom-text-100 outline-none",
|
||||
className
|
||||
"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,
|
||||
}
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{selectedItemIcon && (
|
||||
<div className="flex h-5 w-5 items-center justify-start overflow-hidden">{selectedItemIcon}</div>
|
||||
)}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{selectedItem.title}</div>
|
||||
</li>
|
||||
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
|
||||
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (navigationDisabled) {
|
||||
|
|
@ -46,13 +58,37 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
|
|||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="group flex items-center gap-1.5">
|
||||
<NavigationButton className="cursor-pointer" />
|
||||
<ChevronDownIcon className="size-4 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||
</div>
|
||||
<>
|
||||
<NavigationButton />
|
||||
<Breadcrumbs.Separator
|
||||
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"
|
||||
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
|
||||
menuButtonOnClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{navigationItems.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
|
|
@ -74,7 +110,7 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
|
|||
)}
|
||||
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">
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
|
|
|
|||
96
packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx
Normal file
96
packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx
Normal 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,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -61,6 +61,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
|||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
|
|
@ -95,11 +96,14 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
|||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-1 text-xs",
|
||||
{
|
||||
"cursor-not-allowed text-custom-text-200": disabled,
|
||||
"cursor-pointer hover:bg-custom-background-80": !disabled,
|
||||
},
|
||||
customButtonClassName
|
||||
)}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{customButton}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ const Header = (props: HeaderProps) => {
|
|||
|
||||
const LeftItem = (props: HeaderProps) => (
|
||||
<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}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ export const WorkspaceActiveCycleHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={t("active_cycles")}
|
||||
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ export const WorkspaceAnalyticsHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={t("workspace_analytics.label")}
|
||||
icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,22 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Briefcase } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, LayersIcon, Header, Logo } from "@plane/ui";
|
||||
import { EProjectFeatureKey } from "@plane/constants";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { IssueDetailQuickActions } from "@/components/issues";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
|
||||
export const ProjectIssueDetailsHeader = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, workItem } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { getProjectById, loader } = useProject();
|
||||
const {
|
||||
issue: { getIssueById, getIssueIdByIdentifier },
|
||||
|
|
@ -34,53 +33,20 @@ export const ProjectIssueDetailsHeader = observer(() => {
|
|||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
featureKey={EProjectFeatureKey.WORK_ITEMS}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={projectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
projectDetails ? (
|
||||
projectDetails && (
|
||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||
<Logo logo={projectDetails?.logo_props} size={16} />
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<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}` : ""
|
||||
}
|
||||
label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
{projectId && issueId && (
|
||||
|
|
|
|||
|
|
@ -41,9 +41,8 @@ export const WorkspaceDraftHeader = observer(() => {
|
|||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink label={t("drafts")} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ export const WorkspaceDashboardHeader = () => {
|
|||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -52,9 +52,8 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={breadcrumbLabel}
|
||||
disableTooltip
|
||||
|
|
|
|||
|
|
@ -66,10 +66,9 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
|||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<ProjectBreadcrumb workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
||||
label="Archives"
|
||||
|
|
@ -78,9 +77,8 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
|||
}
|
||||
/>
|
||||
{activeTabBreadcrumbDetail && (
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={activeTabBreadcrumbDetail.label}
|
||||
icon={<activeTabBreadcrumbDetail.icon className="h-4 w-4 text-custom-text-300" />}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,9 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<ProjectBreadcrumb workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
||||
label="Archives"
|
||||
|
|
@ -47,9 +46,8 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
||||
label="Work items"
|
||||
|
|
@ -57,9 +55,8 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={
|
||||
currentProjectDetails && issueDetails
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
|
|
@ -13,8 +12,10 @@ import {
|
|||
EIssuesStoreType,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
EProjectFeatureKey,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
|
|
@ -22,11 +23,11 @@ import {
|
|||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} 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";
|
||||
// components
|
||||
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 { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// hooks
|
||||
|
|
@ -43,9 +44,8 @@ import {
|
|||
} from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
|
|
@ -166,44 +166,31 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<span>
|
||||
<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>
|
||||
}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
featureKey={EProjectFeatureKey.CYCLES}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
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"
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={cycleId}
|
||||
<BreadcrumbNavigationSearchDropdown
|
||||
selectedItem={cycleId}
|
||||
navigationItems={switcherOptions}
|
||||
onChange={(value: string) => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${value}`);
|
||||
}}
|
||||
label={
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={cycleDetails?.name} LabelIcon={ContrastIcon} />
|
||||
title={cycleDetails?.name}
|
||||
icon={
|
||||
<Breadcrumbs.Icon>
|
||||
<ContrastIcon className="size-4 flex-shrink-0 text-custom-text-300" />
|
||||
</Breadcrumbs.Icon>
|
||||
}
|
||||
isLast
|
||||
/>
|
||||
}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
{workItemsCount && workItemsCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
|
|
@ -218,12 +205,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<div className="hidden items-center gap-2 md:flex ">
|
||||
|
|
|
|||
|
|
@ -2,23 +2,25 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { CyclesViewHeader } from "@/components/cycles";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
// constants
|
||||
|
||||
export const CyclesListHeader: FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
// store hooks
|
||||
const { toggleCreateCycleModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
|
@ -35,15 +37,11 @@ export const CyclesListHeader: FC = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={t("cycle.label", { count: 2 })}
|
||||
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={currentProjectDetails?.id ?? ""}
|
||||
featureKey={EProjectFeatureKey.CYCLES}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
|
|
|
|||
|
|
@ -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 items-center gap-2.5">
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label="Draft work items"
|
||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
|
|
@ -14,6 +13,7 @@ import {
|
|||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
EProjectFeatureKey,
|
||||
} from "@plane/constants";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
|
|
@ -21,11 +21,11 @@ import {
|
|||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} 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";
|
||||
// components
|
||||
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";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
|
|
@ -46,7 +46,7 @@ import { useIssuesActions } from "@/hooks/use-issues-actions";
|
|||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
|
|
@ -160,41 +160,28 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
/>
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2 flex-grow">
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<span>
|
||||
<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>
|
||||
}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.MODULES}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
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"
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
label={
|
||||
<div className="flex items-center gap-1">
|
||||
<SwitcherLabel name={moduleDetails?.name} LabelIcon={DiceIcon} />
|
||||
<BreadcrumbNavigationSearchDropdown
|
||||
selectedItem={moduleId?.toString() ?? ""}
|
||||
navigationItems={switcherOptions}
|
||||
onChange={(value: string) => {
|
||||
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}
|
||||
|
|
@ -209,15 +196,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
value={moduleId}
|
||||
onChange={(value: string) => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/modules/${value}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<div className="hidden gap-2 md:flex">
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { ModuleViewHeader } from "@/components/modules";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
|
||||
// constants
|
||||
|
||||
export const ModulesListHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||
// store hooks
|
||||
const { toggleCreateModuleModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
|
@ -39,12 +40,11 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label={t("modules")} icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.MODULES}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
import { EProjectFeatureKey } from "@plane/constants";
|
||||
// types
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||
import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
// components
|
||||
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";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
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";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
|
@ -31,7 +32,7 @@ export const PageDetailsHeader = observer(() => {
|
|||
const router = useAppRouter();
|
||||
const { workspaceSlug, pageId, projectId } = useParams();
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { loader } = useProject();
|
||||
const { getPageById, getCurrentProjectPageIds } = usePageStore(storeType);
|
||||
const page = usePage({
|
||||
pageId: pageId?.toString() ?? "",
|
||||
|
|
@ -64,45 +65,27 @@ export const PageDetailsHeader = observer(() => {
|
|||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<span>
|
||||
<span className="hidden md:block">
|
||||
<ProjectBreadcrumb />
|
||||
</span>
|
||||
<span className="md:hidden">
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
label={"..."}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
featureKey={EProjectFeatureKey.PAGES}
|
||||
/>
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
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"
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
value={pageId}
|
||||
options={switcherOptions}
|
||||
label={
|
||||
<SwitcherLabel logo_props={page.logo_props} name={getPageName(page.name)} LabelIcon={FileText} />
|
||||
}
|
||||
<BreadcrumbNavigationSearchDropdown
|
||||
selectedItem={pageId?.toString() ?? ""}
|
||||
navigationItems={switcherOptions}
|
||||
onChange={(value: string) => {
|
||||
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
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
// constants
|
||||
import { EPageAccess } from "@plane/constants";
|
||||
import { EPageAccess, EProjectFeatureKey } from "@plane/constants";
|
||||
// plane types
|
||||
import { TPage } from "@plane/types";
|
||||
// plane ui
|
||||
import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// hooks
|
||||
import { useEventTracker, useProject } from "@/hooks/store";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
||||
|
|
@ -58,15 +55,14 @@ export const PagesListHeader = observer(() => {
|
|||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Pages" icon={<FileText className="h-4 w-4 text-custom-text-300" />} />}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={currentProjectDetails?.id?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.PAGES}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
{canCurrentUserCreatePage ? (
|
||||
<Header.RightItem>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
EViewAccess,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
EProjectFeatureKey,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
|
|
@ -22,10 +23,10 @@ import {
|
|||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Tooltip, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
// components
|
||||
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";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views";
|
||||
|
|
@ -44,7 +45,7 @@ import {
|
|||
} from "@/hooks/store";
|
||||
// plane web
|
||||
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(() => {
|
||||
// refs
|
||||
|
|
@ -164,27 +165,27 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/views`}
|
||||
label="Views"
|
||||
icon={<Layers className="h-4 w-4 text-custom-text-300" />}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.VIEWS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={viewId}
|
||||
label={<SwitcherLabel logo_props={viewDetails.logo_props} name={viewDetails.name} LabelIcon={Layers} />}
|
||||
<BreadcrumbNavigationSearchDropdown
|
||||
selectedItem={viewId?.toString() ?? ""}
|
||||
navigationItems={switcherOptions}
|
||||
onChange={(value: string) => {
|
||||
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
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Layers } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { EProjectFeatureKey } from "@plane/constants";
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { ViewListHeader } from "@/components/views";
|
||||
// hooks
|
||||
import { useCommandPalette, useProject } from "@/hooks/store";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const ProjectViewsHeader = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||
// store hooks
|
||||
const { toggleCreateViewModal } = useCommandPalette();
|
||||
const { loader } = useProject();
|
||||
|
|
@ -22,10 +23,11 @@ export const ProjectViewsHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.VIEWS}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ export const WorkspaceStickyHeader = observer(() => {
|
|||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={`Stickies`}
|
||||
icon={<RecentStickyIcon className="size-5 rotate-90 text-custom-text-200" />}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
|
|||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { AllIssueLayoutRoot, GlobalViewsAppliedFiltersRoot } from "@/components/issues";
|
||||
import { GlobalViewsHeader } from "@/components/workspace";
|
||||
// constants
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
|
@ -32,7 +31,6 @@ const GlobalViewIssuesPage = observer(() => {
|
|||
<PageHead title={pageTitle} />
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
{globalViewId && (
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} isLoading={isLoading} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,32 +5,49 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { Layers } from "lucide-react";
|
||||
// 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";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
import {
|
||||
CreateUpdateWorkspaceViewModal,
|
||||
WorkspaceViewQuickActions,
|
||||
DefaultWorkspaceViewQuickActions,
|
||||
} from "@/components/workspace";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper";
|
||||
|
||||
export const GlobalIssuesHeader = observer(() => {
|
||||
// states
|
||||
const [createViewModal, setCreateViewModal] = useState(false);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, globalViewId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { filters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { getViewDetailsById } = useGlobalView();
|
||||
const { getViewDetailsById, currentWorkspaceViews } = useGlobalView();
|
||||
const { workspaceLabels } = useLabel();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
|
|
@ -113,6 +130,29 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
|
||||
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 layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET;
|
||||
return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout];
|
||||
|
|
@ -124,9 +164,29 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label={t("views")} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<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>
|
||||
</Header.LeftItem>
|
||||
|
|
@ -171,6 +231,12 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
||||
{t("workspace_views.add_view")}
|
||||
</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>
|
||||
</>
|
||||
|
|
|
|||
32
web/ce/components/breadcrumbs/common.tsx
Normal file
32
web/ce/components/breadcrumbs/common.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +1,3 @@
|
|||
export * from "./common";
|
||||
export * from "./project-feature";
|
||||
export * from "./project";
|
||||
|
|
|
|||
69
web/ce/components/breadcrumbs/project-feature.tsx
Normal file
69
web/ce/components/breadcrumbs/project-feature.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -2,38 +2,73 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { Briefcase } from "lucide-react";
|
||||
// ui
|
||||
import { Breadcrumbs, Logo } from "@plane/ui";
|
||||
// plane imports
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
import { BreadcrumbNavigationSearchDropdown, Breadcrumbs, Logo } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { SwitcherLabel } from "@/components/common";
|
||||
// hooks
|
||||
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
|
||||
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 (
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails ? (
|
||||
currentProjectDetails && (
|
||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
<Briefcase className="h-4 w-4" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbNavigationSearchDropdown
|
||||
selectedItem={currentProjectDetails.id}
|
||||
navigationItems={switcherOptions}
|
||||
onChange={(value: string) => {
|
||||
router.push(`/${workspaceSlug}/projects/${value}/issues`);
|
||||
}}
|
||||
title={currentProjectDetails?.name}
|
||||
icon={renderIcon(currentProjectDetails)}
|
||||
handleOnClick={() => {
|
||||
if (handleOnClick) handleOnClick();
|
||||
else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
showSeparator={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
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
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Tooltip, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, CountChip } from "@/components/common";
|
||||
import { CountChip } from "@/components/common";
|
||||
// constants
|
||||
import HeaderFilters from "@/components/issues/filters";
|
||||
// helpers
|
||||
|
|
@ -20,7 +27,7 @@ import { useIssues } from "@/hooks/store/use-issues";
|
|||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "../breadcrumbs/common";
|
||||
|
||||
export const IssuesHeader = observer(() => {
|
||||
// router
|
||||
|
|
@ -52,18 +59,13 @@ export const IssuesHeader = observer(() => {
|
|||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={t("issue.label", { count: 2 })} // count is for pluralization
|
||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 flex-grow">
|
||||
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"} className="flex-grow-0">
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
featureKey={EProjectFeatureKey.WORK_ITEMS}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
|
|
|
|||
77
web/ce/components/projects/navigation/helper.tsx
Normal file
77
web/ce/components/projects/navigation/helper.tsx
Normal 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,
|
||||
},
|
||||
];
|
||||
1
web/ce/components/projects/navigation/index.ts
Normal file
1
web/ce/components/projects/navigation/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./helper";
|
||||
|
|
@ -5,16 +5,15 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Button, Intake, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { InboxIssueCreateModalRoot } from "@/components/inbox";
|
||||
// hooks
|
||||
import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const ProjectInboxHeader: FC = observer(() => {
|
||||
// states
|
||||
|
|
@ -37,13 +36,13 @@ export const ProjectInboxHeader: FC = observer(() => {
|
|||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4 flex-grow border">
|
||||
<Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label={t("intake")} icon={<Intake className="h-4 w-4 text-custom-text-300" />} />}
|
||||
<CommonProjectBreadcrumbs
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
featureKey={EProjectFeatureKey.INTAKE}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +1,75 @@
|
|||
"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 { Tooltip } from "@plane/ui";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
label?: string | ReactNode;
|
||||
label?: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode | undefined;
|
||||
icon?: React.ReactNode;
|
||||
disableTooltip?: boolean;
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||
const { href, label, icon, disableTooltip = false } = props;
|
||||
const { isMobile } = usePlatformOS();
|
||||
const IconWrapper = React.memo(({ icon }: { icon: React.ReactNode }) => (
|
||||
<div className="flex size-4 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
));
|
||||
|
||||
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 (
|
||||
<Tooltip tooltipContent={label} position="bottom" isMobile={isMobile} disabled={disableTooltip}>
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{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>
|
||||
<>
|
||||
{icon && <IconWrapper icon={icon} />}
|
||||
{label && <LabelWrapper label={label} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,32 @@ import { FC } from "react";
|
|||
import { TLogoProps } from "@plane/types";
|
||||
import { ISvgIcons, Logo } from "@plane/ui";
|
||||
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 = {
|
||||
logo_props?: TLogoProps;
|
||||
logo_url?: string;
|
||||
|
|
@ -13,13 +39,7 @@ export const SwitcherLabel: FC<TSwitcherLabelProps> = (props) => {
|
|||
const { logo_props, name, LabelIcon, logo_url } = props;
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
{logo_props?.in_use ? (
|
||||
<Logo logo={logo_props} size={12} type="lucide" />
|
||||
) : logo_url ? (
|
||||
<img src={getFileURL(logo_url)} alt="logo" className="rounded-sm w-3 h-3 object-cover" />
|
||||
) : (
|
||||
<LabelIcon height={12} width={12} />
|
||||
)}
|
||||
<SwitcherIcon logo_props={logo_props} logo_url={logo_url} LabelIcon={LabelIcon} />
|
||||
{truncateText(name ?? "", 40)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,16 +37,15 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={t("workspace_projects.label", { count: 2 })}
|
||||
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>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
|
|
|
|||
|
|
@ -26,9 +26,8 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
|||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={t("notification.label")}
|
||||
icon={<Inbox className="h-4 w-4 text-custom-text-300" />}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, LinkIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
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";
|
||||
// helpers
|
||||
type Props = {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
workspaceSlug: string;
|
||||
globalViewId: string | undefined;
|
||||
view: {
|
||||
key: TStaticViewTypes;
|
||||
i18n_label: string;
|
||||
|
|
@ -21,7 +18,7 @@ type Props = {
|
|||
};
|
||||
|
||||
export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, globalViewId, view, workspaceSlug } = props;
|
||||
const { workspaceSlug, view } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -53,43 +50,11 @@ export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props
|
|||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ellipsis
|
||||
placement="bottom-end"
|
||||
menuItemsClassName="z-20"
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -37,13 +37,7 @@ const ViewTab = observer((props: { viewId: string }) => {
|
|||
|
||||
return (
|
||||
<div ref={parentRef} className="relative">
|
||||
<WorkspaceViewQuickActions
|
||||
parentRef={parentRef}
|
||||
view={view}
|
||||
viewId={viewId}
|
||||
globalViewId={globalViewId?.toString()}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
/>
|
||||
<WorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={view} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -63,12 +57,7 @@ const DefaultViewTab = (props: {
|
|||
if (!workspaceSlug || !globalViewId) return null;
|
||||
return (
|
||||
<div key={tab.key} ref={parentRef} className="relative">
|
||||
<DefaultWorkspaceViewQuickActions
|
||||
parentRef={parentRef}
|
||||
globalViewId={globalViewId?.toString()}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
view={tab}
|
||||
/>
|
||||
<DefaultWorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={tab} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, LinkIcon, Pencil, Trash2, Lock } from "lucide-react";
|
||||
import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
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";
|
||||
// components
|
||||
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
|
||||
|
|
@ -18,15 +17,12 @@ import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/compone
|
|||
import { useUser, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
workspaceSlug: string;
|
||||
globalViewId: string;
|
||||
viewId: string;
|
||||
view: IWorkspaceView;
|
||||
};
|
||||
|
||||
export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, view, globalViewId, viewId, workspaceSlug } = props;
|
||||
const { workspaceSlug, view } = props;
|
||||
// states
|
||||
const [updateViewModal, setUpdateViewModal] = 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 (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
||||
<DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
|
||||
{customButton}
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue