[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,49 +70,50 @@ 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",
{
"w-64 px-2.5 py-1.5 border-subtle opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-13 text-primary placeholder:text-placeholder focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<CloseIcon className="h-3 w-3" />
</button>
)}
</div>
)}
<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",
{
"w-64 px-2.5 py-1.5 border-subtle opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-13 text-primary placeholder:text-placeholder focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<CloseIcon className="h-3 w-3" />
</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) => ({
...item,
action: () => {
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) {
return {
...item,
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,
})}
/>
{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>
className={cn({
"text-accent-primary bg-accent-subtle border border-accent-subtle-1": showFilterRowChangesPill,
})}
/>
);
});

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 (