mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
[WEB-4546] chore: header enhancements + quickstart guide ui update + breadcrumb #7451
This commit is contained in:
@@ -3,24 +3,25 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Home } from "lucide-react";
|
||||
import { Home, Shapes } from "lucide-react";
|
||||
// images
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
// ui
|
||||
import { GITHUB_REDIRECTED_TRACKER_EVENT, HEADER_GITHUB_ICON } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// constants
|
||||
// hooks
|
||||
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
|
||||
export const WorkspaceDashboardHeader = observer(() => {
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -38,6 +39,15 @@ export const WorkspaceDashboardHeader = observer(() => {
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<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
|
||||
onClick={() =>
|
||||
captureElementAndEvent({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
// plane web components
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
import { ProjectAppSidebar } from "./_sidebar";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
@@ -30,7 +30,13 @@ import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { SwitcherLabel } from "@/components/common";
|
||||
import { CycleQuickActions } from "@/components/cycles";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues";
|
||||
// hooks
|
||||
import {
|
||||
useCommandPalette,
|
||||
@@ -207,21 +213,31 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<div className="hidden items-center gap-2 md:flex ">
|
||||
<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={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
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
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
miniIcon={<ListFilter className="size-3.5" />}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
@@ -238,7 +254,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
@@ -256,7 +276,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<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>
|
||||
{!isCompletedCycle && (
|
||||
<Button
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
IssueLayoutIcon,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
// hooks
|
||||
@@ -108,32 +109,10 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
projectDetails={currentProjectDetails ?? undefined}
|
||||
/>
|
||||
<div className="md:hidden flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
||||
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>
|
||||
<MobileLayoutSelection
|
||||
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
|
||||
onChange={handleLayoutChange}
|
||||
/>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
@@ -27,7 +27,13 @@ import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
// hooks
|
||||
@@ -198,21 +204,31 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<div className="hidden gap-2 md:flex">
|
||||
<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={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
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
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
miniIcon={<ListFilter className="size-3.5" />}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
@@ -229,7 +245,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
@@ -253,7 +273,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
>
|
||||
Analytics
|
||||
<div className="hidden @4xl:flex">Analytics</div>
|
||||
<div className="flex @4xl:hidden">
|
||||
<ChartNoAxesColumn className="size-3.5" />
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden sm:flex"
|
||||
|
||||
@@ -65,6 +65,7 @@ export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => {
|
||||
if (handleOnClick) handleOnClick();
|
||||
else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
|
||||
}}
|
||||
shouldTruncate
|
||||
/>
|
||||
}
|
||||
showSeparator={false}
|
||||
|
||||
16
apps/web/ce/components/common/extended-app-header.tsx
Normal file
16
apps/web/ce/components/common/extended-app-header.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./subscription";
|
||||
export * from "./extended-app-header";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./display-filters";
|
||||
export * from "./filters";
|
||||
export * from "./helpers";
|
||||
export * from "./layout-selection";
|
||||
export * from "./mobile-layout-selection";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
1
apps/web/ee/components/common/extended-app-header.tsx
Normal file
1
apps/web/ee/components/common/extended-app-header.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./extended-app-header";
|
||||
1
apps/web/ee/components/common/index.ts
Normal file
1
apps/web/ee/components/common/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./extended-app-header";
|
||||
@@ -487,6 +487,14 @@ module.exports = {
|
||||
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"]);
|
||||
|
||||
@@ -45,8 +45,11 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
|
||||
}
|
||||
)}
|
||||
>
|
||||
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
|
||||
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
|
||||
<div className="flex @4xl:hidden text-custom-text-300">...</div>
|
||||
<div className="hidden @4xl:flex gap-2">
|
||||
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
|
||||
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ type TBreadcrumbNavigationSearchDropdownProps = {
|
||||
isLast?: boolean;
|
||||
handleOnClick?: () => void;
|
||||
disableRootHover?: boolean;
|
||||
shouldTruncate?: boolean;
|
||||
};
|
||||
|
||||
export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationSearchDropdownProps> = (props) => {
|
||||
@@ -28,6 +29,7 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
|
||||
navigationDisabled = false,
|
||||
isLast = false,
|
||||
handleOnClick,
|
||||
shouldTruncate = false,
|
||||
} = props;
|
||||
// state
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
@@ -65,8 +67,15 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
|
||||
}
|
||||
)}
|
||||
>
|
||||
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>}
|
||||
<Breadcrumbs.Label>{title}</Breadcrumbs.Label>
|
||||
{shouldTruncate && <div className="flex @4xl:hidden text-custom-text-300">...</div>}
|
||||
<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>
|
||||
</Tooltip>
|
||||
<Breadcrumbs.Separator
|
||||
|
||||
@@ -24,5 +24,5 @@ export const minHeights: IHeaderProperties = {
|
||||
export const getHeaderStyle = (variant: THeaderVariant, setMinHeight: boolean, showOnMobile: boolean) => {
|
||||
const height = setMinHeight ? minHeights[variant] : "";
|
||||
const display = variant === EHeaderVariant.SECONDARY ? (showOnMobile ? "flex" : "hidden md:flex") : "";
|
||||
return " " + headerStyle[variant] + " " + height + " " + display;
|
||||
return " @container " + headerStyle[variant] + " " + height + " " + display;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user