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