[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

@ -5,9 +5,7 @@ import { observer } from "mobx-react";
// plane imports
import { Row } from "@plane/ui";
// components
import { AppSidebarToggleButton } from "@/components/sidebar";
// hooks
import { useAppTheme } from "@/hooks/store";
import { ExtendedAppHeader } from "@/plane-web/components/common";
export interface AppHeaderProps {
header: ReactNode;
@ -16,14 +14,11 @@ export interface AppHeaderProps {
export const AppHeader = observer((props: AppHeaderProps) => {
const { header, mobileHeader } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<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">
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
<ExtendedAppHeader header={header} />
</Row>
{mobileHeader && mobileHeader}
</div>

View file

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

View file

@ -1,20 +1,17 @@
import { FC } from "react";
import { Shapes } from "lucide-react";
// plane types
import { useTranslation } from "@plane/i18n";
import { IUser } from "@plane/types";
// plane ui
import { Button } from "@plane/ui";
// hooks
import { useCurrentTime } from "@/hooks/use-current-time";
export interface IUserGreetingsView {
user: IUser;
handleWidgetModal: () => void;
}
export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const { user, handleWidgetModal } = props;
const { user } = props;
// current time hook
const { currentTime } = useCurrentTime();
// 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";
return (
<div className="flex justify-between">
<div>
<h3 className="text-xl font-semibold text-center">
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="flex items-center gap-2 font-medium text-custom-text-400">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</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 className="flex flex-col items-center my-6">
<h3 className="text-xl font-semibold text-center">
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="flex items-center gap-2 font-medium text-custom-text-400">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</h6>
</div>
);
};

View file

@ -44,7 +44,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "create-project",
title: "home.empty.create_project.title",
description: "home.empty.create_project.description",
icon: <Briefcase className="size-10" />,
icon: <Briefcase className="size-4" />,
flag: "projects",
cta: {
text: "home.empty.create_project.cta",
@ -62,7 +62,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "invite-team",
title: "home.empty.invite_team.title",
description: "home.empty.invite_team.description",
icon: <Users className="size-10" />,
icon: <Users className="size-4" />,
flag: "visited_members",
cta: {
text: "home.empty.invite_team.cta",
@ -74,7 +74,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "configure-workspace",
title: "home.empty.configure_workspace.title",
description: "home.empty.configure_workspace.description",
icon: <Hotel className="size-10" />,
icon: <Hotel className="size-4" />,
flag: "visited_workspace",
cta: {
text: "home.empty.configure_workspace.cta",
@ -89,7 +89,7 @@ export const NoProjectsEmptyState = observer(() => {
icon:
currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? (
<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
src={getFileURL(currentUser?.avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
@ -99,7 +99,7 @@ export const NoProjectsEmptyState = observer(() => {
</Link>
) : (
<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]}
</span>
</Link>
@ -142,17 +142,17 @@ export const NoProjectsEmptyState = observer(() => {
{t("home.empty.not_right_now")}
</button>
</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) => {
const isStateComplete = isComplete(item.flag);
return (
<div
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
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,
}
@ -160,10 +160,10 @@ export const NoProjectsEmptyState = observer(() => {
>
<span className="text-3xl my-auto">{item.icon}</span>
</div>
<h3 className="text-base 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>
<h3 className="text-sm font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-[11px] text-custom-text-300 mb-2">{t(item.description)}</p>
{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" />
</div>
) : (

View file

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

View file

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

View file

@ -2,3 +2,4 @@ export * from "./display-filters";
export * from "./filters";
export * from "./helpers";
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>
);
};