[WEB-5614] chore: platform header and breadcrumb enhancements (#8383)

This commit is contained in:
Anmol Singh Bhatia 2025-12-18 18:39:06 +05:30 committed by GitHub
parent 3df58397b5
commit b165e2a3fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 163 additions and 85 deletions

View file

@ -14,6 +14,7 @@ import {
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { IconButton } from "@plane/propel/icon-button";
import { CycleIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
@ -236,7 +237,6 @@ export const CycleIssuesHeader = observer(function CycleIssuesHeader() {
<Button
variant="primary"
size="lg"
className="self-start"
onClick={() => {
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}}
@ -247,9 +247,15 @@ export const CycleIssuesHeader = observer(function CycleIssuesHeader() {
)}
</>
)}
<Button variant="ghost" size="lg" onClick={toggleSidebar}>
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-accent-primary" : "text-secondary")} />
</Button>
<IconButton
variant="tertiary"
size="lg"
icon={PanelRight}
onClick={toggleSidebar}
className={cn({
"text-accent-primary bg-accent-subtle": !isSidebarCollapsed,
})}
/>
<CycleQuickActions
parentRef={parentRef}
cycleId={cycleId}

View file

@ -42,6 +42,7 @@ import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web imports
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
import { IconButton } from "@plane/propel/icon-button";
export const ModuleIssuesHeader = observer(function ModuleIssuesHeader() {
// refs
@ -242,9 +243,15 @@ export const ModuleIssuesHeader = observer(function ModuleIssuesHeader() {
) : (
<></>
)}
<Button variant="ghost" size="lg" onClick={toggleSidebar}>
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-accent-primary" : "text-secondary")} />
</Button>
<IconButton
variant="tertiary"
size="lg"
icon={PanelRight}
onClick={toggleSidebar}
className={cn({
"text-accent-primary bg-accent-subtle": !isSidebarCollapsed,
})}
/>
{moduleId && (
<ModuleQuickActions
parentRef={parentRef}

View file

@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import { Disclosure } from "@headlessui/react";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { ICycle } from "@plane/types";
@ -15,7 +16,6 @@ import { ActiveCycleProgress } from "@/components/cycles/active-cycle/progress";
import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details";
import { CycleListGroupHeader } from "@/components/cycles/list/cycle-list-group-header";
import { CyclesListItem } from "@/components/cycles/list/cycles-list-item";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import type { ActiveCycleIssueDetails } from "@/store/issue/cycle";
@ -50,10 +50,11 @@ const ActiveCyclesComponent = observer(function ActiveCyclesComponent({
if (!cycleId || !activeCycle) {
return (
<DetailedEmptyState
<EmptyStateDetailed
assetKey="cycle"
title={t("project_cycles.empty_state.active.title")}
description={t("project_cycles.empty_state.active.description")}
assetPath={activeCycleResolvedPath}
rootClassName="py-10 h-auto"
/>
);
}
@ -114,7 +115,7 @@ export const ActiveCycleRoot = observer(function ActiveCycleRoot(props: IActiveC
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-subtle bg-layer-2 cursor-pointer">
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-subtle bg-layer-1 cursor-pointer">
<CycleListGroupHeader title={t("project_cycles.active_cycle.label")} type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>

View file

@ -70,19 +70,18 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
}, [searchQuery]);
return (
<div className="flex items-center gap-3">
{!isSearchOpen && (
<div className="flex items-center gap-2">
{!isSearchOpen ? (
<IconButton
variant="ghost"
size="lg"
className="-mr-5"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
icon={Search}
/>
)}
) : (
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-surface-1 text-placeholder w-0 transition-[width] ease-linear overflow-hidden opacity-0",
@ -113,6 +112,8 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
</button>
)}
</div>
)}
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title={t("common.filters")}

View file

@ -301,7 +301,7 @@ export const CycleListItemAction = observer(function CycleListItemAction(props:
{createdByDetails && !isActive && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
{!isActive && (
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
<div className="flex w-min cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => {

View file

@ -40,7 +40,7 @@ export const CyclesList = observer(function CyclesList(props: ICyclesList) {
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-subtle bg-layer-2 cursor-pointer">
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-subtle bg-layer-1 cursor-pointer">
<CycleListGroupHeader
title={t("project_cycles.upcoming_cycle.label")}
type="upcoming"
@ -59,7 +59,7 @@ export const CyclesList = observer(function CyclesList(props: ICyclesList) {
<Disclosure as="div" className="flex flex-shrink-0 flex-col pb-7">
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-2 w-full flex-shrink-0 border-b border-subtle bg-layer-2 cursor-pointer">
<Disclosure.Button className="sticky top-0 z-2 w-full flex-shrink-0 border-b border-subtle bg-layer-1 cursor-pointer">
<CycleListGroupHeader
title={t("project_cycles.completed_cycle.label")}
type="completed"

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { MoreHorizontal } from "lucide-react";
// ui
import {
@ -9,6 +10,7 @@ import {
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IconButton } from "@plane/propel/icon-button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui";
import { ContextMenu, CustomMenu } from "@plane/ui";
@ -155,7 +157,13 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop
</div>
)}
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect maxHeight="lg" buttonClassName={customClassName}>
<CustomMenu
customButton={<IconButton variant="tertiary" size="lg" icon={MoreHorizontal} />}
placement="bottom-end"
closeOnSelect
maxHeight="lg"
buttonClassName={customClassName}
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View file

@ -94,7 +94,7 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0 || displayFilters?.favorites;
return (
<div className="hidden h-full sm:flex items-center gap-3 self-end">
<div className="hidden h-full sm:flex items-center gap-2 self-end">
<div className="flex items-center">
{!isSearchOpen && (
<IconButton

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { MoreHorizontal } from "lucide-react";
// plane imports
import {
@ -9,6 +10,7 @@ import {
MODULE_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IconButton } from "@plane/propel/icon-button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui";
import { ContextMenu, CustomMenu } from "@plane/ui";
@ -112,15 +114,18 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr
const MENU_ITEMS: TContextMenuItem[] = Array.isArray(menuResult) ? menuResult : menuResult.items;
const additionalModals = Array.isArray(menuResult) ? null : menuResult.modals;
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) {
return {
...item,
action: () => {
onClick: () => {
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
};
});
return (
<>
@ -145,7 +150,12 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr
</div>
)}
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
<CustomMenu
customButton={<IconButton variant="tertiary" size="lg" icon={MoreHorizontal} />}
placement="bottom-end"
closeOnSelect
buttonClassName={customClassName}
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View file

@ -1,9 +1,10 @@
import { observer } from "mobx-react";
import { ListFilter } from "lucide-react";
// plane imports
import { IconButton } from "@plane/propel/icon-button";
import { FilterIcon, FilterAppliedIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
import type { IFilterInstance } from "@plane/shared-state";
import type { TExternalFilter, TFilterProperty } from "@plane/types";
import { cn } from "@plane/ui";
// components
import { AddFilterButton } from "@/components/rich-filters/add-filters/button";
@ -49,28 +50,14 @@ export const FiltersToggle = observer(function FiltersToggle<P extends TFilterPr
}
return (
<button
className={cn(COMMON_CLASSNAME, {
"border-transparent bg-accent-primary/10 hover:bg-accent-primary/20": isFilterRowVisible,
"hover:bg-surface-1": !isFilterRowVisible,
})}
<IconButton
size="lg"
variant="secondary"
icon={showFilterRowChangesPill ? FilterAppliedIcon : FilterIcon}
onClick={handleToggleFilter}
>
<div className="relative">
<ListFilter
className={cn("size-4", {
"text-accent-primary": isFilterRowVisible,
"text-tertiary": !isFilterRowVisible,
className={cn({
"text-accent-primary bg-accent-subtle border border-accent-subtle-1": showFilterRowChangesPill,
})}
/>
{showFilterRowChangesPill && (
<span
className={cn("p-[3px] rounded-full bg-accent-primary absolute top-[0.2px] -right-[0.4px]", {
"bg-layer-1": hasAnyConditions === false && filter?.hasChanges === true, // If there are no conditions and there are changes, show the pill in the background color
})}
/>
)}
</div>
</button>
);
});

View file

@ -1,7 +1,9 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { MoreHorizontal } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { IconButton } from "@plane/propel/icon-button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProjectView } from "@plane/types";
// ui
@ -96,7 +98,12 @@ export const ViewQuickActions = observer(function ViewQuickActions(props: Props)
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
{additionalModals}
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
<CustomMenu
customButton={<IconButton variant="tertiary" size="lg" icon={MoreHorizontal} />}
placement="bottom-end"
closeOnSelect
buttonClassName={customClassName}
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View file

@ -0,0 +1,21 @@
import * as React from "react";
import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";
export function FilterAppliedIcon({ color = "text-icon-brand", ...rest }: ISvgIcons) {
const clipPathId = React.useId();
return (
<IconWrapper color={color} clipPathId={clipPathId} {...rest}>
<path
d="M15.3 3.65C15.3 5.11355 14.1136 6.3 12.65 6.3C11.1864 6.3 10 5.11355 10 3.65C10 2.18645 11.1864 1 12.65 1C14.1136 1 15.3 2.18645 15.3 3.65Z"
fill={color}
/>
<path
d="M9.99984 12.3333C10.368 12.3333 10.6665 12.6318 10.6665 13C10.6665 13.3682 10.368 13.6667 9.99984 13.6667H5.99984C5.63165 13.6667 5.33317 13.3682 5.33317 13C5.33317 12.6318 5.63165 12.3333 5.99984 12.3333H9.99984ZM11.9998 7.66667C12.368 7.66667 12.6665 7.96514 12.6665 8.33333C12.6665 8.70152 12.368 9 11.9998 9H3.99984C3.63165 9 3.33317 8.70152 3.33317 8.33333C3.33317 7.96514 3.63165 7.66667 3.99984 7.66667H11.9998ZM7.99984 3C8.36803 3 8.6665 3.29848 8.6665 3.66667C8.6665 4.03486 8.36803 4.33333 7.99984 4.33333H1.33317C0.964981 4.33333 0.666504 4.03486 0.666504 3.66667C0.666504 3.29848 0.964981 3 1.33317 3H7.99984Z"
fill={color}
/>
</IconWrapper>
);
}

View file

@ -0,0 +1,17 @@
import * as React from "react";
import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";
export function FilterIcon({ color = "currentColor", ...rest }: ISvgIcons) {
const clipPathId = React.useId();
return (
<IconWrapper color={color} clipPathId={clipPathId} {...rest}>
<path
d="M9.9751 11.3496C10.3203 11.3496 10.6001 11.6294 10.6001 11.9746C10.6001 12.3198 10.3203 12.5996 9.9751 12.5996H5.9751C5.62992 12.5996 5.3501 12.3198 5.3501 11.9746C5.3501 11.6294 5.62992 11.3496 5.9751 11.3496H9.9751ZM11.9751 7.34961C12.3203 7.34961 12.6001 7.62943 12.6001 7.97461C12.6001 8.31979 12.3203 8.59961 11.9751 8.59961H3.9751C3.62992 8.59961 3.3501 8.31979 3.3501 7.97461C3.3501 7.62943 3.62992 7.34961 3.9751 7.34961H11.9751ZM13.9751 3.34961C14.3203 3.34961 14.6001 3.62943 14.6001 3.97461C14.6001 4.31979 14.3203 4.59961 13.9751 4.59961H1.9751C1.62992 4.59961 1.3501 4.31979 1.3501 3.97461C1.3501 3.62943 1.62992 3.34961 1.9751 3.34961H13.9751Z"
fill={color}
/>
</IconWrapper>
);
}

View file

@ -3,6 +3,8 @@ export * from "./add-icon";
export * from "./add-workitem-icon";
export * from "./add-reaction-icon";
export * from "./close-icon";
export * from "./filter-icon";
export * from "./filter-applied-icon";
export * from "./search-icon";
export * from "./preferences-icon";
export * from "./copy-link";

View file

@ -4,6 +4,8 @@ export const ActionsIconsMap = [
{ icon: <Icon name="action.add-workitem" />, title: "AddWorkItemIcon" },
{ icon: <Icon name="action.add-reaction" />, title: "AddReactionIcon" },
{ icon: <Icon name="action.close" />, title: "CloseIcon" },
{ icon: <Icon name="action.filter" />, title: "FilterIcon" },
{ icon: <Icon name="action.filter-applied" />, title: "FilterAppliedIcon" },
{ icon: <Icon name="action.search" />, title: "SearchIcon" },
{ icon: <Icon name="action.preferences" />, title: "PreferencesIcon" },
{ icon: <Icon name="action.copy-link" />, title: "CopyLinkIcon" },

View file

@ -1,4 +1,11 @@
import { AddReactionIcon, AddWorkItemIcon, PreferencesIcon, SearchIcon } from "./actions";
import {
AddReactionIcon,
AddWorkItemIcon,
FilterAppliedIcon,
FilterIcon,
PreferencesIcon,
SearchIcon,
} from "./actions";
import { AddIcon } from "./actions/add-icon";
import { CloseIcon } from "./actions/close-icon";
import { ChevronDownIcon } from "./arrows/chevron-down";
@ -121,6 +128,8 @@ export const ICON_REGISTRY = {
"action.add-workitem": AddWorkItemIcon,
"action.add-reaction": AddReactionIcon,
"action.close": CloseIcon,
"action.filter": FilterIcon,
"action.filter-applied": FilterAppliedIcon,
"action.search": SearchIcon,
"action.preferences": PreferencesIcon,
"action.copy-link": CopyLinkIcon,