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

* chore: project feature enum added

* feat: revamp breadcrumb and add navigation dropdown component

* chore: custom search select component refactoring

* chore: breadcrumb stories added

* chore: switch label and breadcrumb link component refactor

* chore: project navigation helper function added

* chore: common breadcrumb component added

* chore: breadcrumb refactoring

* chore: code refactor

* chore: code refactor

* fix: build error

* fix: nprogress and button tooltip

* chore: code refactor

* chore: workspace view breadcrumb improvements

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

---------

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

View file

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

View file

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