[WEB-4546] chore: header enhancements + quickstart guide ui update + breadcrumb #7451

This commit is contained in:
Akshita Goyal 2025-07-22 16:47:14 +05:30 committed by GitHub
parent 5c22a6cecc
commit 763a28ab60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 307 additions and 148 deletions

View file

@ -3,24 +3,25 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Home } from "lucide-react"; import { Home, Shapes } from "lucide-react";
// images // images
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
// ui // ui
import { GITHUB_REDIRECTED_TRACKER_EVENT, HEADER_GITHUB_ICON } from "@plane/constants"; import { GITHUB_REDIRECTED_TRACKER_EVENT, HEADER_GITHUB_ICON } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
// constants // constants
// hooks // hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper"; import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useHome } from "@/hooks/store/use-home";
export const WorkspaceDashboardHeader = observer(() => { export const WorkspaceDashboardHeader = observer(() => {
// hooks // hooks
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const { toggleWidgetSettings } = useHome();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -38,6 +39,15 @@ export const WorkspaceDashboardHeader = observer(() => {
</div> </div>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem> <Header.RightItem>
<Button
variant="neutral-primary"
size="sm"
onClick={() => toggleWidgetSettings(true)}
className="my-auto mb-0"
>
<Shapes size={16} />
<div className="text-xs font-medium">{t("home.manage_widgets")}</div>
</Button>
<a <a
onClick={() => onClick={() =>
captureElementAndEvent({ captureElementAndEvent({

View file

@ -2,6 +2,7 @@
import { CommandPalette } from "@/components/command-palette"; import { CommandPalette } from "@/components/command-palette";
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web components
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
import { ProjectAppSidebar } from "./_sidebar"; import { ProjectAppSidebar } from "./_sidebar";

View file

@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// icons // icons
import { PanelRight } from "lucide-react"; import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
// plane imports // plane imports
import { import {
EIssueFilterType, EIssueFilterType,
@ -30,7 +30,13 @@ import { cn, isIssueFilterActive } from "@plane/utils";
import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { 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,
MobileLayoutSelection,
} from "@/components/issues";
// hooks // hooks
import { import {
useCommandPalette, useCommandPalette,
@ -207,21 +213,31 @@ export const CycleIssuesHeader: React.FC = observer(() => {
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem className="items-center"> <Header.RightItem className="items-center">
<div className="hidden items-center gap-2 md:flex "> <div className="hidden items-center gap-2 md:flex ">
<LayoutSelection <div className="hidden @4xl:flex">
layouts={[ <LayoutSelection
EIssueLayoutTypes.LIST, layouts={[
EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.LIST,
EIssueLayoutTypes.CALENDAR, EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.SPREADSHEET, EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.GANTT, EIssueLayoutTypes.SPREADSHEET,
]} EIssueLayoutTypes.GANTT,
onChange={(layout) => handleLayoutChange(layout)} ]}
selectedLayout={activeLayout} onChange={(layout) => handleLayoutChange(layout)}
/> selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown <FiltersDropdown
title={t("common.filters")} title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)} isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
> >
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
@ -238,7 +254,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view} moduleViewDisabled={!currentProjectDetails?.module_view}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end"> <FiltersDropdown
title={t("common.display")}
placement="bottom-end"
miniIcon={<SlidersHorizontal className="size-3.5" />}
>
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
@ -256,7 +276,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue && ( {canUserCreateIssue && (
<> <>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
{t("common.analytics")} <div className="hidden @4xl:flex">Analytics</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button> </Button>
{!isCompletedCycle && ( {!isCompletedCycle && (
<Button <Button

View file

@ -24,6 +24,7 @@ import {
FilterSelection, FilterSelection,
FiltersDropdown, FiltersDropdown,
IssueLayoutIcon, IssueLayoutIcon,
MobileLayoutSelection,
} from "@/components/issues/issue-layouts"; } from "@/components/issues/issue-layouts";
// helpers // helpers
// hooks // hooks
@ -108,32 +109,10 @@ export const ProjectIssuesMobileHeader = observer(() => {
projectDetails={currentProjectDetails ?? undefined} projectDetails={currentProjectDetails ?? undefined}
/> />
<div className="md:hidden flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100"> <div className="md:hidden flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100">
<CustomMenu <MobileLayoutSelection
maxHeight={"md"} layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
className="flex flex-grow justify-center text-sm text-custom-text-200" onChange={handleLayoutChange}
placement="bottom-start" />
customButton={
<div className="flex flex-start text-sm text-custom-text-200">
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
key={index}
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200"> <div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown <FiltersDropdown
title={t("common.filters")} title={t("common.filters")}

View file

@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// icons // icons
import { PanelRight } from "lucide-react"; import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
// plane imports // plane imports
import { import {
EIssueFilterType, EIssueFilterType,
@ -27,7 +27,13 @@ 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 { SwitcherLabel } from "@/components/common"; import { SwitcherLabel } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues";
// helpers // helpers
import { ModuleQuickActions } from "@/components/modules"; import { ModuleQuickActions } from "@/components/modules";
// hooks // hooks
@ -198,21 +204,31 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
</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">
<LayoutSelection <div className="hidden @4xl:flex">
layouts={[ <LayoutSelection
EIssueLayoutTypes.LIST, layouts={[
EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.LIST,
EIssueLayoutTypes.CALENDAR, EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.SPREADSHEET, EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.GANTT, EIssueLayoutTypes.SPREADSHEET,
]} EIssueLayoutTypes.GANTT,
onChange={(layout) => handleLayoutChange(layout)} ]}
selectedLayout={activeLayout} onChange={(layout) => handleLayoutChange(layout)}
/> selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown <FiltersDropdown
title="Filters" title="Filters"
placement="bottom-end" placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)} isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
> >
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
@ -229,7 +245,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view} moduleViewDisabled={!currentProjectDetails?.module_view}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown
title="Display"
placement="bottom-end"
miniIcon={<SlidersHorizontal className="size-3.5" />}
>
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
@ -253,7 +273,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
variant="neutral-primary" variant="neutral-primary"
size="sm" size="sm"
> >
Analytics <div className="hidden @4xl:flex">Analytics</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button> </Button>
<Button <Button
className="hidden sm:flex" className="hidden sm:flex"

View file

@ -65,6 +65,7 @@ export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => {
if (handleOnClick) handleOnClick(); if (handleOnClick) handleOnClick();
else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`); else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
}} }}
shouldTruncate
/> />
} }
showSeparator={false} showSeparator={false}

View file

@ -0,0 +1,16 @@
import { ReactNode } from "react";
import { AppSidebarToggleButton } from "@/components/sidebar";
import { useAppTheme } from "@/hooks/store/use-app-theme";
export const ExtendedAppHeader = (props: { header: ReactNode }) => {
const { header } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<>
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
</>
);
};

View file

@ -1 +1,2 @@
export * from "./subscription"; export * from "./subscription";
export * from "./extended-app-header";

View file

@ -5,9 +5,7 @@ import { observer } from "mobx-react";
// plane imports // plane imports
import { Row } from "@plane/ui"; import { Row } from "@plane/ui";
// components // components
import { AppSidebarToggleButton } from "@/components/sidebar"; import { ExtendedAppHeader } from "@/plane-web/components/common";
// hooks
import { useAppTheme } from "@/hooks/store";
export interface AppHeaderProps { export interface AppHeaderProps {
header: ReactNode; header: ReactNode;
@ -16,14 +14,11 @@ export interface AppHeaderProps {
export const AppHeader = observer((props: AppHeaderProps) => { export const AppHeader = observer((props: AppHeaderProps) => {
const { header, mobileHeader } = props; const { header, mobileHeader } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return ( return (
<div className="z-[18]"> <div className="z-[18]">
<Row className="h-header flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100"> <Row className="h-header flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100">
{sidebarCollapsed && <AppSidebarToggleButton />} <ExtendedAppHeader header={header} />
<div className="w-full">{header}</div>
</Row> </Row>
{mobileHeader && mobileHeader} {mobileHeader && mobileHeader}
</div> </div>

View file

@ -12,7 +12,6 @@ import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks // hooks
import { useUserProfile, useUser } from "@/hooks/store"; import { useUserProfile, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home"; import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
// plane web components // plane web components
import { HomePeekOverviewsRoot } from "@/plane-web/components/home"; import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
// local imports // local imports
@ -24,8 +23,7 @@ export const WorkspaceHomeView = observer(() => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile(); const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { toggleWidgetSettings, fetchWidgets } = useHome(); const { fetchWidgets } = useHome();
const [windowWidth] = useSize();
useSWR( useSWR(
workspaceSlug ? `HOME_DASHBOARD_WIDGETS_${workspaceSlug}` : null, workspaceSlug ? `HOME_DASHBOARD_WIDGETS_${workspaceSlug}` : null,
@ -62,12 +60,8 @@ export const WorkspaceHomeView = observer(() => {
)} )}
<> <>
<HomePeekOverviewsRoot /> <HomePeekOverviewsRoot />
<ContentWrapper <ContentWrapper className={cn("gap-6 bg-custom-background-100 max-w-[750px] mx-auto scrollbar-hide")}>
className={cn("gap-6 bg-custom-background-90/20", { {currentUser && <UserGreetingsView user={currentUser} />}
"vertical-scrollbar scrollbar-lg": windowWidth >= 768,
})}
>
{currentUser && <UserGreetingsView user={currentUser} handleWidgetModal={() => toggleWidgetSettings(true)} />}
<DashboardWidgets /> <DashboardWidgets />
</ContentWrapper> </ContentWrapper>
</> </>

View file

@ -1,20 +1,17 @@
import { FC } from "react"; import { FC } from "react";
import { Shapes } from "lucide-react";
// plane types // plane types
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IUser } from "@plane/types"; import { IUser } from "@plane/types";
// plane ui // plane ui
import { Button } from "@plane/ui";
// hooks // hooks
import { useCurrentTime } from "@/hooks/use-current-time"; import { useCurrentTime } from "@/hooks/use-current-time";
export interface IUserGreetingsView { export interface IUserGreetingsView {
user: IUser; user: IUser;
handleWidgetModal: () => void;
} }
export const UserGreetingsView: FC<IUserGreetingsView> = (props) => { export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const { user, handleWidgetModal } = props; const { user } = props;
// current time hook // current time hook
const { currentTime } = useCurrentTime(); const { currentTime } = useCurrentTime();
// store hooks // store hooks
@ -44,22 +41,16 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening"; const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
return ( return (
<div className="flex justify-between"> <div className="flex flex-col items-center my-6">
<div> <h3 className="text-xl font-semibold text-center">
<h3 className="text-xl font-semibold text-center"> {t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name} </h3>
</h3> <h6 className="flex items-center gap-2 font-medium text-custom-text-400">
<h6 className="flex items-center gap-2 font-medium text-custom-text-400"> <div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div> <div>
<div> {weekDay}, {date} {timeString}
{weekDay}, {date} {timeString} </div>
</div> </h6>
</h6>
</div>
<Button variant="neutral-primary" size="sm" onClick={handleWidgetModal} className="my-auto mb-0">
<Shapes size={16} />
<div className="text-xs font-medium">{t("home.manage_widgets")}</div>
</Button>
</div> </div>
); );
}; };

View file

@ -44,7 +44,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "create-project", id: "create-project",
title: "home.empty.create_project.title", title: "home.empty.create_project.title",
description: "home.empty.create_project.description", description: "home.empty.create_project.description",
icon: <Briefcase className="size-10" />, icon: <Briefcase className="size-4" />,
flag: "projects", flag: "projects",
cta: { cta: {
text: "home.empty.create_project.cta", text: "home.empty.create_project.cta",
@ -62,7 +62,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "invite-team", id: "invite-team",
title: "home.empty.invite_team.title", title: "home.empty.invite_team.title",
description: "home.empty.invite_team.description", description: "home.empty.invite_team.description",
icon: <Users className="size-10" />, icon: <Users className="size-4" />,
flag: "visited_members", flag: "visited_members",
cta: { cta: {
text: "home.empty.invite_team.cta", text: "home.empty.invite_team.cta",
@ -74,7 +74,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "configure-workspace", id: "configure-workspace",
title: "home.empty.configure_workspace.title", title: "home.empty.configure_workspace.title",
description: "home.empty.configure_workspace.description", description: "home.empty.configure_workspace.description",
icon: <Hotel className="size-10" />, icon: <Hotel className="size-4" />,
flag: "visited_workspace", flag: "visited_workspace",
cta: { cta: {
text: "home.empty.configure_workspace.cta", text: "home.empty.configure_workspace.cta",
@ -89,7 +89,7 @@ export const NoProjectsEmptyState = observer(() => {
icon: icon:
currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? ( currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}> <Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full p-4 capitalize text-white"> <span className="relative flex size-4 items-center justify-center rounded-full p-4 capitalize text-white">
<img <img
src={getFileURL(currentUser?.avatar_url)} src={getFileURL(currentUser?.avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover" className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
@ -99,7 +99,7 @@ export const NoProjectsEmptyState = observer(() => {
</Link> </Link>
) : ( ) : (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}> <Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm"> <span className="relative flex size-4 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm">
{(currentUser?.email ?? currentUser?.display_name ?? "?")[0]} {(currentUser?.email ?? currentUser?.display_name ?? "?")[0]}
</span> </span>
</Link> </Link>
@ -142,17 +142,17 @@ export const NoProjectsEmptyState = observer(() => {
{t("home.empty.not_right_now")} {t("home.empty.not_right_now")}
</button> </button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{EMPTY_STATE_DATA.map((item) => { {EMPTY_STATE_DATA.map((item) => {
const isStateComplete = isComplete(item.flag); const isStateComplete = isComplete(item.flag);
return ( return (
<div <div
key={item.id} key={item.id}
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40" className="flex flex-col p-4 bg-custom-background-100 rounded-xl border border-custom-border-200/40"
> >
<div <div
className={cn( className={cn(
"grid place-items-center bg-custom-background-90 rounded-full size-20 mb-3 text-custom-text-400", "grid place-items-center bg-custom-background-90 rounded-full size-9 mb-3 text-custom-text-400",
{ {
"text-custom-primary-100 bg-custom-primary-100/10": !isStateComplete, "text-custom-primary-100 bg-custom-primary-100/10": !isStateComplete,
} }
@ -160,10 +160,10 @@ export const NoProjectsEmptyState = observer(() => {
> >
<span className="text-3xl my-auto">{item.icon}</span> <span className="text-3xl my-auto">{item.icon}</span>
</div> </div>
<h3 className="text-base font-medium text-custom-text-100 mb-2">{t(item.title)}</h3> <h3 className="text-sm font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-sm text-custom-text-300 mb-2">{t(item.description)}</p> <p className="text-[11px] text-custom-text-300 mb-2">{t(item.description)}</p>
{isStateComplete ? ( {isStateComplete ? (
<div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1"> <div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1 w-fit">
<Check className="size-3 text-custom-primary-100 text-white" /> <Check className="size-3 text-custom-primary-100 text-white" />
</div> </div>
) : ( ) : (

View file

@ -17,13 +17,21 @@ import {
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { isIssueFilterActive } from "@plane/utils"; import { isIssueFilterActive } from "@plane/utils";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
IssueLayoutIcon,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues";
// helpers // helpers
// hooks // hooks
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
// plane web types // plane web types
import { TProject } from "@/plane-web/types"; import { TProject } from "@/plane-web/types";
import { WorkItemsModal } from "../analytics/work-items/modal"; import { WorkItemsModal } from "../analytics/work-items/modal";
import { ChartNoAxesColumn, ChevronDown, ListFilter, SlidersHorizontal } from "lucide-react";
type Props = { type Props = {
currentProjectDetails: TProject | undefined; currentProjectDetails: TProject | undefined;
@ -32,6 +40,13 @@ type Props = {
canUserCreateIssue: boolean | undefined; canUserCreateIssue: boolean | undefined;
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC; storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;
}; };
const LAYOUTS = [
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
];
const HeaderFilters = observer((props: Props) => { const HeaderFilters = observer((props: Props) => {
const { const {
currentProjectDetails, currentProjectDetails,
@ -109,21 +124,25 @@ const HeaderFilters = observer((props: Props) => {
projectDetails={currentProjectDetails ?? undefined} projectDetails={currentProjectDetails ?? undefined}
isEpic={storeType === EIssuesStoreType.EPIC} isEpic={storeType === EIssuesStoreType.EPIC}
/> />
<LayoutSelection <div className="hidden @4xl:flex">
layouts={[ <LayoutSelection
EIssueLayoutTypes.LIST, layouts={LAYOUTS}
EIssueLayoutTypes.KANBAN, onChange={(layout) => handleLayoutChange(layout)}
EIssueLayoutTypes.CALENDAR, selectedLayout={activeLayout}
EIssueLayoutTypes.SPREADSHEET, />
EIssueLayoutTypes.GANTT, </div>
]} <div className="flex @4xl:hidden">
onChange={(layout) => handleLayoutChange(layout)} <MobileLayoutSelection
selectedLayout={activeLayout} layouts={LAYOUTS}
/> onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown <FiltersDropdown
title={t("common.filters")} title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)} isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
> >
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
@ -140,7 +159,11 @@ const HeaderFilters = observer((props: Props) => {
isEpic={storeType === EIssuesStoreType.EPIC} isEpic={storeType === EIssuesStoreType.EPIC}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end"> <FiltersDropdown
miniIcon={<SlidersHorizontal className="size-3.5" />}
title={t("common.display")}
placement="bottom-end"
>
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions} layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
@ -153,8 +176,16 @@ const HeaderFilters = observer((props: Props) => {
/> />
</FiltersDropdown> </FiltersDropdown>
{canUserCreateIssue ? ( {canUserCreateIssue ? (
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button
{t("common.analytics")} className="hidden md:block px-2"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
<div className="hidden @4xl:flex">{t("common.analytics")}</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button> </Button>
) : ( ) : (
<></> <></>

View file

@ -13,6 +13,7 @@ import { Button } from "@plane/ui";
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
icon?: React.ReactNode; icon?: React.ReactNode;
miniIcon?: React.ReactNode;
title?: string; title?: string;
placement?: Placement; placement?: Placement;
disabled?: boolean; disabled?: boolean;
@ -24,6 +25,7 @@ type Props = {
export const FiltersDropdown: React.FC<Props> = (props) => { export const FiltersDropdown: React.FC<Props> = (props) => {
const { const {
children, children,
miniIcon,
icon, icon,
title = "Dropdown", title = "Dropdown",
placement, placement,
@ -33,7 +35,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
isFiltersApplied = false, isFiltersApplied = false,
} = props; } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
@ -53,27 +55,42 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
{menuButton} {menuButton}
</button> </button>
) : ( ) : (
<Button <div ref={setReferenceElement}>
disabled={disabled} <div className="hidden @4xl:flex">
ref={setReferenceElement} <Button
variant="neutral-primary" disabled={disabled}
size="sm" variant="neutral-primary"
prependIcon={icon} size="sm"
appendIcon={ prependIcon={icon}
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} /> appendIcon={
} <ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
tabIndex={tabIndex} }
className="relative" tabIndex={tabIndex}
> className="relative"
<> >
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}> <>
<span>{title}</span> <div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
</div> <span>{title}</span>
{isFiltersApplied && ( </div>
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" /> {isFiltersApplied && (
)} <span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
</> )}
</Button> </>
</Button>
</div>
<div className="flex @4xl:hidden">
<Button
disabled={disabled}
ref={setReferenceElement}
variant="neutral-primary"
size="sm"
tabIndex={tabIndex}
className="relative px-2"
>
{miniIcon || title}
</Button>
</div>
</div>
)} )}
</Popover.Button> </Popover.Button>
<Transition <Transition

View file

@ -2,3 +2,4 @@ export * from "./display-filters";
export * from "./filters"; export * from "./filters";
export * from "./helpers"; export * from "./helpers";
export * from "./layout-selection"; export * from "./layout-selection";
export * from "./mobile-layout-selection";

View file

@ -0,0 +1,54 @@
import { ChevronDown } from "lucide-react";
import { ISSUE_LAYOUTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes } from "@plane/types";
import { Button, CustomMenu } from "@plane/ui";
import { IssueLayoutIcon } from "../../layout-icon";
export const MobileLayoutSelection = ({
layouts,
onChange,
activeLayout,
}: {
layouts: EIssueLayoutTypes[];
onChange: (layout: EIssueLayoutTypes) => void;
activeLayout?: EIssueLayoutTypes;
isMobile?: boolean;
}) => {
const { t } = useTranslation();
return (
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
activeLayout ? (
<Button variant="neutral-primary" size="sm" className="relative px-2">
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className={`h-3.5 w-3.5`} />
<ChevronDown className="size-3 text-custom-text-200 my-auto" strokeWidth={2} />
</Button>
) : (
<div className="flex flex-start text-sm text-custom-text-200">
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
)
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout, index) => (
<CustomMenu.MenuItem
key={index}
onClick={() => {
onChange(layout.key);
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={layout.key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
);
};

View file

@ -0,0 +1 @@
export * from "./extended-app-header";

View file

@ -0,0 +1 @@
export * from "./extended-app-header";

View file

@ -487,6 +487,14 @@ module.exports = {
paddingRight: "1.35rem", paddingRight: "1.35rem",
}, },
}, },
// Hide scrollbar but keep functionality
".scrollbar-hide": {
"-ms-overflow-style": "none" /* IE and Edge */,
"scrollbar-width": "none" /* Firefox */,
"&::-webkit-scrollbar": {
display: "none" /* Chrome, Safari and Opera */,
},
},
}; };
addUtilities(newUtilities, ["responsive"]); addUtilities(newUtilities, ["responsive"]);

View file

@ -45,8 +45,11 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
} }
)} )}
> >
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>} <div className="flex @4xl:hidden text-custom-text-300">...</div>
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label> <div className="hidden @4xl:flex gap-2">
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
</div>
</button> </button>
</Tooltip> </Tooltip>
); );

View file

@ -16,6 +16,7 @@ type TBreadcrumbNavigationSearchDropdownProps = {
isLast?: boolean; isLast?: boolean;
handleOnClick?: () => void; handleOnClick?: () => void;
disableRootHover?: boolean; disableRootHover?: boolean;
shouldTruncate?: boolean;
}; };
export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationSearchDropdownProps> = (props) => { export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationSearchDropdownProps> = (props) => {
@ -28,6 +29,7 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
navigationDisabled = false, navigationDisabled = false,
isLast = false, isLast = false,
handleOnClick, handleOnClick,
shouldTruncate = false,
} = props; } = props;
// state // state
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@ -65,8 +67,15 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
} }
)} )}
> >
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>} {shouldTruncate && <div className="flex @4xl:hidden text-custom-text-300">...</div>}
<Breadcrumbs.Label>{title}</Breadcrumbs.Label> <div
className={cn("flex gap-2", {
"hidden @4xl:flex gap-2": shouldTruncate,
})}
>
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{title}</Breadcrumbs.Label>
</div>
</button> </button>
</Tooltip> </Tooltip>
<Breadcrumbs.Separator <Breadcrumbs.Separator

View file

@ -24,5 +24,5 @@ export const minHeights: IHeaderProperties = {
export const getHeaderStyle = (variant: THeaderVariant, setMinHeight: boolean, showOnMobile: boolean) => { export const getHeaderStyle = (variant: THeaderVariant, setMinHeight: boolean, showOnMobile: boolean) => {
const height = setMinHeight ? minHeights[variant] : ""; const height = setMinHeight ? minHeights[variant] : "";
const display = variant === EHeaderVariant.SECONDARY ? (showOnMobile ? "flex" : "hidden md:flex") : ""; const display = variant === EHeaderVariant.SECONDARY ? (showOnMobile ? "flex" : "hidden md:flex") : "";
return " " + headerStyle[variant] + " " + height + " " + display; return " @container " + headerStyle[variant] + " " + height + " " + display;
}; };