[WEB-4546] chore: header enhancements + quickstart guide ui update + breadcrumb #7451
This commit is contained in:
parent
5c22a6cecc
commit
763a28ab60
22 changed files with 307 additions and 148 deletions
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
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 "./subscription";
|
||||||
|
export * from "./extended-app-header";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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",
|
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"]);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue