style: responsive breadcrumbs and headers for dashboard, projects, project issues, cycles, cycle issues, module issues (#3580)

This commit is contained in:
Ramesh Kumar Chandra 2024-02-07 13:44:03 +05:30 committed by GitHub
parent 751b15a7a7
commit 4b2a9c8335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1065 additions and 500 deletions

View File

@ -38,13 +38,11 @@ export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
> >
<Dialog.Panel className="fixed inset-0 z-20 h-full w-full overflow-y-auto"> <Dialog.Panel className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
<div <div
className={`fixed right-0 top-0 z-20 h-full bg-custom-background-100 shadow-custom-shadow-md ${ className={`fixed right-0 top-0 z-20 h-full bg-custom-background-100 shadow-custom-shadow-md ${fullScreen ? "w-full p-2" : "w-full sm:w-full md:w-1/2"
fullScreen ? "w-full p-2" : "w-1/2"
}`} }`}
> >
<div <div
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${ className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${fullScreen ? "rounded-lg border" : "border-l"
fullScreen ? "rounded-lg border" : "border-l"
}`} }`}
> >
<ProjectAnalyticsModalHeader <ProjectAnalyticsModalHeader

View File

@ -0,0 +1,168 @@
import { useCallback, useState } from "react";
import router from "next/router";
//components
import { CustomMenu } from "@plane/ui";
// icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// hooks
import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue";
import { ProjectAnalyticsModal } from "components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
export const CycleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const { getCycleById } = useCycle();
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
},
[workspaceSlug, projectId, cycleId, updateFilters]
);
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId);
},
[workspaceSlug, projectId, cycleId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId);
},
[workspaceSlug, projectId, cycleId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
cycleDetails={cycleDetails ?? undefined}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
<span
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</span>
</div>
</>
);
};

View File

@ -159,10 +159,10 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90"> <div className="group flex flex-col md:flex-row w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="flex w-full items-center gap-3 truncate"> <div className="relative w-full flex items-center justify-between gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate"> <div className="relative w-full flex items-center gap-3 overflow-hidden">
<span className="flex-shrink-0"> <div className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}> <CircularProgressIndicator size={38} percentage={progress}>
{isCompleted ? ( {isCompleted ? (
progress === 100 ? ( progress === 100 ? (
@ -176,27 +176,25 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<span className="text-xs text-custom-text-300">{`${progress}%`}</span> <span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)} )}
</CircularProgressIndicator> </CircularProgressIndicator>
</span> </div>
<div className="flex items-center gap-2.5"> <div className="relative flex items-center gap-2.5 overflow-hidden">
<span className="flex-shrink-0"> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span>
<Tooltip tooltipContent={cycleDetails.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycleDetails.name}</span> <span className="truncate line-clamp-1 inline-block overflow-hidden text-base font-medium">
{cycleDetails.name}
</span>
</Tooltip> </Tooltip>
</div> </div>
</div>
<button onClick={openCycleOverview} className="z-10 hidden flex-shrink-0 group-hover:flex"> <button onClick={openCycleOverview} className="flex-shrink-0 z-10 invisible group-hover:visible">
<Info className="h-4 w-4 text-custom-text-400" /> <Info className="h-4 w-4 text-custom-text-400" />
</button> </button>
</div> </div>
<div className="flex w-full items-center justify-end gap-2.5 md:w-auto md:flex-shrink-0 ">
<div className="flex items-center justify-center">
{currentCycle && ( {currentCycle && (
<span <div
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs" className="flex-shrink-0 relative flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
style={{ style={{
color: currentCycle.color, color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`, backgroundColor: `${currentCycle.color}20`,
@ -205,18 +203,17 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
{currentCycle.value === "current" {currentCycle.value === "current"
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left` ? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </div>
)} )}
</div> </div>
<div className="flex-shrink-0 relative overflow-hidden flex w-full items-center justify-between md:justify-end gap-2.5 md:w-auto md:flex-shrink-0 ">
<div className="text-xs text-custom-text-300">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
{renderDate && ( <div className="flex-shrink-0 relative flex items-center gap-3">
<span className="flex w-40 items-center justify-center gap-2 text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex w-16 cursor-default items-center justify-center gap-1"> <div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignees.length > 0 ? ( {cycleDetails.assignees.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycleDetails.assignees.map((assignee) => ( {cycleDetails.assignees.map((assignee) => (
@ -230,8 +227,10 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
)} )}
</div> </div>
</Tooltip> </Tooltip>
{isEditingAllowed &&
(cycleDetails.is_favorite ? ( {isEditingAllowed && (
<>
{cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}> <button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" /> <Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button> </button>
@ -239,7 +238,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<button type="button" onClick={handleAddToFavorites}> <button type="button" onClick={handleAddToFavorites}>
<Star className="h-3.5 w-3.5 text-custom-text-200" /> <Star className="h-3.5 w-3.5 text-custom-text-200" />
</button> </button>
))} )}
<CustomMenu ellipsis> <CustomMenu ellipsis>
{!isCompleted && isEditingAllowed && ( {!isCompleted && isEditingAllowed && (
@ -265,6 +264,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</>
)}
</div>
</div> </div>
</div> </div>
</Link> </Link>

View File

@ -23,7 +23,7 @@ import { BreadcrumbLink } from "components/common";
// ui // ui
import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui";
// icons // icons
import { ArrowRight, Plus } from "lucide-react"; import { ArrowRight, Plus, PanelRight } from "lucide-react";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
@ -32,6 +32,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants // constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper";
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
// router // router
@ -147,13 +149,16 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
cycleDetails={cycleDetails ?? undefined} cycleDetails={cycleDetails ?? undefined}
/> />
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 w-full items-center gap-x-2 gap-y-4">
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={ link={
<span>
<span className="hidden md:block">
<BreadcrumbLink <BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
@ -169,6 +174,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
) )
} }
/> />
</span>
<Link href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} className="block md:hidden pl-2 text-custom-text-300">...</Link>
</span>
} }
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
@ -202,7 +210,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
/> />
</Breadcrumbs> </Breadcrumbs>
</div> </div>
<div className="flex items-center gap-2"> <div className="hidden md:flex items-center gap-2 ">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
@ -257,7 +265,20 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} /> <ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
</button> </button>
</div> </div>
<button
type="button"
className="grid md:hidden h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar}
>
<PanelRight className={cn("w-4 h-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
</button>
</div>
<div className="block sm:block md:hidden">
<CycleMobileHeader />
</div>
</div> </div>
</> </>
); );
}); });

View File

@ -1,22 +1,24 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { List, Plus } from "lucide-react";
// hooks // hooks
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
// ui // ui
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui"; import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
// components // components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { TCycleLayout } from "@plane/types";
import { CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import useLocalStorage from "hooks/use-local-storage";
export const CyclesHeader: FC = observer(() => { export const CyclesHeader: FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks // store hooks
const { const {
commandPalette: { toggleCreateCycleModal }, commandPalette: { toggleCreateCycleModal },
@ -30,8 +32,21 @@ export const CyclesHeader: FC = observer(() => {
const canUserCreateCycle = const canUserCreateCycle =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const { workspaceSlug } = router.query as {
workspaceSlug: string;
};
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
const handleCurrentLayout = useCallback(
(_layout: TCycleLayout) => {
setCycleLayout(_layout);
},
[setCycleLayout]
);
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 items-center justify-between gap-x-2 gap-y-4">
<div className="flex border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div>
@ -79,5 +94,34 @@ export const CyclesHeader: FC = observer(() => {
</div> </div>
)} )}
</div> </div>
<div className="flex justify-center sm:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
// placement="bottom-start"
customButton={
<span className="flex items-center gap-2">
<List className="h-4 w-4" />
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
</span>
}
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
closeOnSelect
>
{CYCLE_VIEW_LAYOUTS.map((layout) => (
<CustomMenu.MenuItem
onClick={() => {
// handleLayoutChange(ISSUE_LAYOUTS[index].key);
handleCurrentLayout(layout.key as TCycleLayout);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
); );
}); });

View File

@ -23,7 +23,7 @@ import { BreadcrumbLink } from "components/common";
// ui // ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
// icons // icons
import { ArrowRight, Plus } from "lucide-react"; import { ArrowRight, PanelRight, Plus } from "lucide-react";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
@ -32,6 +32,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants // constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper";
import { ModuleMobileHeader } from "components/modules/module-mobile-header";
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
// router // router
@ -150,13 +152,16 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined} moduleDetails={moduleDetails ?? undefined}
/> />
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 items-center gap-x-2 gap-y-4">
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={ link={
<span>
<span className="hidden md:block">
<BreadcrumbLink <BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
@ -172,6 +177,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
) )
} }
/> />
</span>
<Link href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} className="block md:hidden pl-2 text-custom-text-300">...</Link>
</span>
} }
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
@ -206,6 +214,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="hidden md:flex gap-2">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
@ -234,10 +243,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties} handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
</div>
{canUserCreateIssue && ( {canUserCreateIssue && (
<> <>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics Analytics
</Button> </Button>
<Button <Button
@ -248,7 +258,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
size="sm" size="sm"
prependIcon={<Plus />} prependIcon={<Plus />}
> >
Add Issue <span className="hidden md:block">Add</span> Issue
</Button> </Button>
</> </>
)} )}
@ -257,10 +267,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80" className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar} onClick={toggleSidebar}
> >
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} /> <ArrowRight className={`h-4 w-4 duration-300 hidden md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
<PanelRight className={cn("w-4 h-4 block md:hidden", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
</button> </button>
</div> </div>
</div> </div>
<ModuleMobileHeader />
</div>
</> </>
); );
}); });

View File

@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
// hooks // hooks
import { import {
useApplication, useApplication,
@ -29,6 +29,7 @@ import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } f
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { useIssues } from "hooks/store/use-issues"; import { useIssues } from "hooks/store/use-issues";
import { IssuesMobileHeader } from "components/issues/issues-mobile-header";
export const ProjectIssuesHeader: React.FC = observer(() => { export const ProjectIssuesHeader: React.FC = observer(() => {
// states // states
@ -114,18 +115,10 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined} projectDetails={currentProjectDetails ?? undefined}
/> />
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className=" relative z-10 items-center gap-x-2 gap-y-4">
<div className="flex items-center gap-2 p-4 border-b border-custom-border-200 bg-custom-sidebar-background-100">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
@ -178,7 +171,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</a> </a>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="items-center gap-2 hidden md:flex">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
@ -223,9 +216,10 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</Link> </Link>
)} )}
</div>
{canUserCreateIssue && ( {canUserCreateIssue && (
<> <>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics Analytics
</Button> </Button>
<Button <Button
@ -241,6 +235,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</> </>
)} )}
</div> </div>
<div className="block md:hidden">
<IssuesMobileHeader />
</div>
</div> </div>
</> </>
); );

View File

@ -23,7 +23,7 @@ export const ProjectsHeader = observer(() => {
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div>
<Breadcrumbs> <Breadcrumbs>
@ -34,12 +34,12 @@ export const ProjectsHeader = observer(() => {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex w-full justify-end items-center gap-3">
{workspaceProjectIds && workspaceProjectIds?.length > 0 && ( {workspaceProjectIds && workspaceProjectIds?.length > 0 && (
<div className="flex w-full items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400"> <div className=" flex items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400">
<Search className="h-3.5 w-3.5" /> <Search className="h-3.5" />
<input <input
className="w-full min-w-[234px] border-none bg-transparent text-sm focus:outline-none" className="border-none w-full bg-transparent text-sm focus:outline-none"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search" placeholder="Search"
@ -54,6 +54,7 @@ export const ProjectsHeader = observer(() => {
setTrackElement("Projects page"); setTrackElement("Projects page");
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}} }}
className="items-center"
> >
Add Project Add Project
</Button> </Button>

View File

@ -16,15 +16,6 @@ export const WorkspaceAnalyticsHeader = () => {
> >
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem

View File

@ -37,13 +37,13 @@ export const WorkspaceDashboardHeader = () => {
href="https://plane.so/changelog" href="https://plane.so/changelog"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5 text-xs font-medium" className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
> >
<Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" /> <Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" />
{"What's new?"} <span className="text-xs hidden sm:hidden md:block font-medium">{"What's new?"}</span>
</a> </a>
<a <a
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5 text-xs font-medium" className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5 "
href="https://github.com/makeplane/plane" href="https://github.com/makeplane/plane"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -54,7 +54,7 @@ export const WorkspaceDashboardHeader = () => {
width={16} width={16}
alt="GitHub Logo" alt="GitHub Logo"
/> />
Star us on GitHub <span className="text-xs font-medium hidden sm:hidden md:block">Star us on GitHub</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -13,10 +13,11 @@ type Props = {
placement?: Placement; placement?: Placement;
disabled?: boolean; disabled?: boolean;
tabIndex?: number; tabIndex?: number;
menuButton?: React.ReactNode;
}; };
export const FiltersDropdown: React.FC<Props> = (props) => { export const FiltersDropdown: React.FC<Props> = (props) => {
const { children, title = "Dropdown", placement, disabled = false, tabIndex } = props; const { children, title = "Dropdown", placement, disabled = false, tabIndex, menuButton } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -33,7 +34,9 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
return ( return (
<> <>
<Popover.Button as={React.Fragment}> <Popover.Button as={React.Fragment}>
<Button {menuButton ? <button role="button" ref={setReferenceElement}>
{menuButton}
</button> : <Button
disabled={disabled} disabled={disabled}
ref={setReferenceElement} ref={setReferenceElement}
variant="neutral-primary" variant="neutral-primary"
@ -46,7 +49,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}> <div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
<span>{title}</span> <span>{title}</span>
</div> </div>
</Button> </Button>}
</Popover.Button> </Popover.Button>
<Transition <Transition
as={Fragment} as={Fragment}

View File

@ -0,0 +1,166 @@
import { useCallback, useState } from "react";
import router from "next/router";
// components
import { CustomMenu } from "@plane/ui";
// icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// layouts
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectAnalyticsModal } from "components/analytics";
export const IssuesMobileHeader = () => {
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const [analyticsModal, setAnalyticsModal] = useState(false);
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
};
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
const {
project: { projectMemberIds },
} = useMember();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</>
);
};

View File

@ -0,0 +1,162 @@
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue";
import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store";
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
import router from "next/router";
import { useCallback, useState } from "react";
export const ModuleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const { getModuleById } = useModule();
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const activeLayout = issueFilters?.displayFilters?.layout;
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</>
);
};

View File

@ -108,14 +108,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
> >
<div className="flex flex-col items-end justify-between gap-4 border-b border-custom-border-200 px-4 pb-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0"> <div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll"> <Tab.List as="div" className="flex items-center overflow-x-scroll">
{CYCLE_TAB_LIST.map((tab) => ( {CYCLE_TAB_LIST.map((tab) => (
<Tab <Tab
key={tab.key} key={tab.key}
className={({ selected }) => className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${ `border-b-2 p-4 text-sm font-medium outline-none ${selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}` }`
} }
> >
@ -123,8 +122,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
<div className="hidden sm:block">
{cycleTab !== "active" && ( {cycleTab !== "active" && (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1"> <div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => { {CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key === "gantt" && cycleTab === "draft") return null; if (layout.key === "gantt" && cycleTab === "draft") return null;
@ -132,15 +132,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tooltip key={layout.key} tooltipContent={layout.title}> <Tooltip key={layout.key} tooltipContent={layout.title}>
<button <button
type="button" type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`} }`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)} onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
> >
<layout.icon <layout.icon
strokeWidth={2} strokeWidth={2}
className={`h-3.5 w-3.5 ${ className={`h-3.5 w-3.5 ${cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
/> />
</button> </button>
@ -150,6 +148,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</div> </div>
)} )}
</div> </div>
</div>
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full overflow-y-auto"> <Tab.Panel as="div" className="h-full overflow-y-auto">