[WEB-2273] Chore: header UI (#5467)
* chore: headers + common containers * fix: filters code splitting * fix: home header * fix: header changes * fix: uncommented filters * fix: used enums * fix: enum changes
This commit is contained in:
parent
747905a96d
commit
22656d0114
64 changed files with 1356 additions and 1119 deletions
|
|
@ -333,6 +333,8 @@ module.exports = {
|
||||||
72: "16.2rem",
|
72: "16.2rem",
|
||||||
80: "18rem",
|
80: "18rem",
|
||||||
96: "21.6rem",
|
96: "21.6rem",
|
||||||
|
"page-x": "1.35rem",
|
||||||
|
"page-y": "1.35rem"
|
||||||
},
|
},
|
||||||
margin: {
|
margin: {
|
||||||
0: "0",
|
0: "0",
|
||||||
|
|
@ -434,5 +436,23 @@ module.exports = {
|
||||||
custom: ["Inter", "sans-serif"],
|
custom: ["Inter", "sans-serif"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography"), function({ addUtilities }) {
|
||||||
|
const newUtilities = {
|
||||||
|
// Mobile screens
|
||||||
|
'.px-page-x': {
|
||||||
|
paddingLeft: '0.675rem',
|
||||||
|
paddingRight: '0.675rem',
|
||||||
|
},
|
||||||
|
// Medium screens (768px and up)
|
||||||
|
'@media (min-width: 768px)': {
|
||||||
|
'.px-page-x': {
|
||||||
|
paddingLeft: '1.35rem',
|
||||||
|
paddingRight: '1.35rem',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addUtilities(newUtilities, ['responsive']);
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
39
packages/ui/src/header/header.tsx
Normal file
39
packages/ui/src/header/header.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../helpers";
|
||||||
|
import { EHeaderVariant, getHeaderStyle, THeaderVariant } from "./helper";
|
||||||
|
import { ERowVariant, CustomRow } from "../row";
|
||||||
|
|
||||||
|
export interface CustomHeaderProps {
|
||||||
|
variant?: THeaderVariant;
|
||||||
|
setHeight?: boolean;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomHeader = (props: CustomHeaderProps) => {
|
||||||
|
const { variant = EHeaderVariant.PRIMARY, className = "", setHeight = true, children, ...rest } = props;
|
||||||
|
|
||||||
|
const style = getHeaderStyle(variant, setHeight);
|
||||||
|
return (
|
||||||
|
<CustomRow
|
||||||
|
variant={variant === EHeaderVariant.PRIMARY ? ERowVariant.HUGGING : ERowVariant.REGULAR}
|
||||||
|
className={cn(style, className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CustomRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LeftItem = (props: CustomHeaderProps) => (
|
||||||
|
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">{props.children}</div>
|
||||||
|
);
|
||||||
|
const RightItem = (props: CustomHeaderProps) => (
|
||||||
|
<div className="w-full flex items-center justify-end gap-3">{props.children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomHeader.LeftItem = LeftItem;
|
||||||
|
CustomHeader.RightItem = RightItem;
|
||||||
|
CustomHeader.displayName = "plane-ui-header";
|
||||||
|
|
||||||
|
export { CustomHeader, EHeaderVariant };
|
||||||
25
packages/ui/src/header/helper.tsx
Normal file
25
packages/ui/src/header/helper.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
export enum EHeaderVariant {
|
||||||
|
PRIMARY = "primary",
|
||||||
|
SECONDARY = "secondary",
|
||||||
|
TERNARY = "ternary",
|
||||||
|
}
|
||||||
|
export type THeaderVariant = EHeaderVariant.PRIMARY | EHeaderVariant.SECONDARY | EHeaderVariant.TERNARY;
|
||||||
|
|
||||||
|
export interface IHeaderProperties {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
export const headerStyle: IHeaderProperties = {
|
||||||
|
[EHeaderVariant.PRIMARY]:
|
||||||
|
"relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100",
|
||||||
|
[EHeaderVariant.SECONDARY]: "block !py-0 overflow-y-hidden border-b border-custom-border-200",
|
||||||
|
[EHeaderVariant.TERNARY]: "flex justify-between py-2 border-b border-custom-border-200",
|
||||||
|
};
|
||||||
|
export const minHeights: IHeaderProperties = {
|
||||||
|
[EHeaderVariant.PRIMARY]: "",
|
||||||
|
[EHeaderVariant.SECONDARY]: "min-h-[52px]",
|
||||||
|
[EHeaderVariant.TERNARY]: "",
|
||||||
|
};
|
||||||
|
export const getHeaderStyle = (variant: THeaderVariant, setMinHeight: boolean) => {
|
||||||
|
const height = setMinHeight ? minHeights[variant] : "";
|
||||||
|
return headerStyle[variant] + " " + height;
|
||||||
|
};
|
||||||
1
packages/ui/src/header/index.ts
Normal file
1
packages/ui/src/header/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./header";
|
||||||
|
|
@ -23,3 +23,5 @@ export * from "./loader";
|
||||||
export * from "./collapsible";
|
export * from "./collapsible";
|
||||||
export * from "./popovers";
|
export * from "./popovers";
|
||||||
export * from "./tables";
|
export * from "./tables";
|
||||||
|
export * from "./header";
|
||||||
|
export * from "./row";
|
||||||
|
|
|
||||||
12
packages/ui/src/row/helper.tsx
Normal file
12
packages/ui/src/row/helper.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export enum ERowVariant {
|
||||||
|
REGULAR = "regular",
|
||||||
|
HUGGING = "hugging",
|
||||||
|
}
|
||||||
|
export type TRowVariant = ERowVariant.REGULAR | ERowVariant.HUGGING;
|
||||||
|
export interface IRowProperties {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
export const rowStyle: IRowProperties = {
|
||||||
|
[ERowVariant.REGULAR]: "px-page-x",
|
||||||
|
[ERowVariant.HUGGING]: "px-0",
|
||||||
|
};
|
||||||
1
packages/ui/src/row/index.ts
Normal file
1
packages/ui/src/row/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./row";
|
||||||
25
packages/ui/src/row/row.tsx
Normal file
25
packages/ui/src/row/row.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../helpers";
|
||||||
|
import { ERowVariant, rowStyle, TRowVariant } from "./helper";
|
||||||
|
|
||||||
|
export interface CustomRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
variant?: TRowVariant;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomRow = React.forwardRef<HTMLDivElement, CustomRowProps>((props, ref) => {
|
||||||
|
const { variant = ERowVariant.REGULAR, className = "", children, ...rest } = props;
|
||||||
|
|
||||||
|
const style = rowStyle[variant];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={cn(style, className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CustomRow.displayName = "plane-ui-row";
|
||||||
|
|
||||||
|
export { CustomRow, ERowVariant };
|
||||||
|
|
@ -2,29 +2,27 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, ContrastIcon } from "@plane/ui";
|
import { Breadcrumbs, ContrastIcon, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { UpgradeBadge } from "@/plane-web/components/workspace";
|
import { UpgradeBadge } from "@/plane-web/components/workspace";
|
||||||
|
|
||||||
export const WorkspaceActiveCycleHeader = observer(() => (
|
export const WorkspaceActiveCycleHeader = observer(() => (
|
||||||
<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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<Breadcrumbs>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
label="Active Cycles"
|
||||||
label="Active Cycles"
|
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
|
||||||
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
<UpgradeBadge size="md" />
|
||||||
<UpgradeBadge size="md" />
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useSearchParams } from "next/navigation";
|
||||||
// icons
|
// icons
|
||||||
import { BarChart2, PanelRight } from "lucide-react";
|
import { BarChart2, PanelRight } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -36,38 +36,32 @@ export const WorkspaceAnalyticsHeader = observer(() => {
|
||||||
}, [toggleWorkspaceAnalyticsSidebar, workspaceAnalyticsSidebarCollapsed]);
|
}, [toggleWorkspaceAnalyticsSidebar, workspaceAnalyticsSidebarCollapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CustomHeader>
|
||||||
<div
|
<CustomHeader.LeftItem>
|
||||||
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 bg-custom-sidebar-background-100 p-4`}
|
<Breadcrumbs>
|
||||||
>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
type="text"
|
||||||
<div className="flex w-full items-center justify-between">
|
link={<BreadcrumbLink label="Analytics" icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
<Breadcrumbs>
|
/>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
</Breadcrumbs>
|
||||||
type="text"
|
{analytics_tab === "custom" ? (
|
||||||
link={
|
<button
|
||||||
<BreadcrumbLink label="Analytics" icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} />
|
className="block md:hidden"
|
||||||
}
|
onClick={() => {
|
||||||
/>
|
toggleWorkspaceAnalyticsSidebar();
|
||||||
</Breadcrumbs>
|
}}
|
||||||
{analytics_tab === "custom" && (
|
>
|
||||||
<button
|
<PanelRight
|
||||||
className="block md:hidden"
|
className={cn(
|
||||||
onClick={() => {
|
"block h-4 w-4 md:hidden",
|
||||||
toggleWorkspaceAnalyticsSidebar();
|
!workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200"
|
||||||
}}
|
)}
|
||||||
>
|
/>
|
||||||
<PanelRight
|
</button>
|
||||||
className={cn(
|
) : (
|
||||||
"block h-4 w-4 md:hidden",
|
<></>
|
||||||
!workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200"
|
)}
|
||||||
)}
|
</CustomHeader.LeftItem>
|
||||||
/>
|
</CustomHeader>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
|
@ -34,24 +35,26 @@ const AnalyticsPage = observer(() => {
|
||||||
{workspaceProjectIds.length > 0 || loader ? (
|
{workspaceProjectIds.length > 0 || loader ? (
|
||||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-5">
|
<CustomHeader variant={EHeaderVariant.SECONDARY}>
|
||||||
{ANALYTICS_TABS.map((tab) => (
|
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200">
|
||||||
<Tab key={tab.key} as={Fragment}>
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
{({ selected }) => (
|
<Tab key={tab.key} as={Fragment}>
|
||||||
<button
|
{({ selected }) => (
|
||||||
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
<button
|
||||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
||||||
}`}
|
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
||||||
>
|
}`}
|
||||||
{tab.title}
|
>
|
||||||
<div
|
{tab.title}
|
||||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
<div
|
||||||
/>
|
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
||||||
</button>
|
/>
|
||||||
)}
|
</button>
|
||||||
</Tab>
|
)}
|
||||||
))}
|
</Tab>
|
||||||
</Tab.List>
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
</CustomHeader>
|
||||||
<Tab.Panels as={Fragment}>
|
<Tab.Panels as={Fragment}>
|
||||||
<Tab.Panel as={Fragment}>
|
<Tab.Panel as={Fragment}>
|
||||||
<ScopeAndDemand fullScreen />
|
<ScopeAndDemand fullScreen />
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Home } from "lucide-react";
|
||||||
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 { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -22,8 +22,8 @@ export const WorkspaceDashboardHeader = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -32,8 +32,8 @@ export const WorkspaceDashboardHeader = () => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-3 px-3">
|
<CustomHeader.RightItem>
|
||||||
<a
|
<a
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
captureEvent(GITHUB_REDIRECTED, {
|
captureEvent(GITHUB_REDIRECTED, {
|
||||||
|
|
@ -53,8 +53,8 @@ export const WorkspaceDashboardHeader = () => {
|
||||||
/>
|
/>
|
||||||
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
|
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { ChevronDown, PanelRight } from "lucide-react";
|
import { ChevronDown, PanelRight } from "lucide-react";
|
||||||
import { IUserProfileProjectSegregation } from "@plane/types";
|
import { IUserProfileProjectSegregation } from "@plane/types";
|
||||||
import { Breadcrumbs, CustomMenu, UserActivityIcon } from "@plane/ui";
|
import { Breadcrumbs, CustomHeader, CustomMenu, UserActivityIcon } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// components
|
// components
|
||||||
import { ProfileIssuesFilter } from "@/components/profile";
|
import { ProfileIssuesFilter } from "@/components/profile";
|
||||||
|
|
@ -46,8 +46,8 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||||
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
|
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -104,7 +104,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useParams, usePathname } from "next/navigation";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
// constants
|
// constants
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
|
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -20,7 +21,7 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky -top-0.5 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="sticky -top-0.5 hidden md:flex md:static">
|
||||||
<div className="flex items-center overflow-x-scroll">
|
<div className="flex items-center overflow-x-scroll">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||||
|
|
@ -36,6 +37,6 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, Breadcrumbs, Tooltip } from "@plane/ui";
|
import { ArchiveIcon, Breadcrumbs, Tooltip, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -16,8 +16,8 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
type TProps = {
|
type TProps = {
|
||||||
activeTab: 'issues' | 'cycles' | 'modules';
|
activeTab: "issues" | "cycles" | "modules";
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
||||||
const { activeTab } = props;
|
const { activeTab } = props;
|
||||||
|
|
@ -38,8 +38,8 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
||||||
PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST];
|
PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST];
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -92,7 +92,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui";
|
import { ArchiveIcon, Breadcrumbs, LayersIcon, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { IssueDetailQuickActions } from "@/components/issues";
|
import { IssueDetailQuickActions } from "@/components/issues";
|
||||||
|
|
@ -36,66 +36,66 @@ export const ProjectArchivedIssueDetailsHeader = 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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
href={`/${workspaceSlug}/projects`}
|
||||||
href={`/${workspaceSlug}/projects`}
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
icon={
|
||||||
icon={
|
currentProjectDetails && (
|
||||||
currentProjectDetails && (
|
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
</span>
|
||||||
</span>
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
label="Archives"
|
||||||
label="Archives"
|
icon={<ArchiveIcon className="h-4 w-4 text-custom-text-300" />}
|
||||||
icon={<ArchiveIcon className="h-4 w-4 text-custom-text-300" />}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
|
label="Issues"
|
||||||
label="Issues"
|
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
label={
|
||||||
label={
|
currentProjectDetails && issueDetails
|
||||||
currentProjectDetails && issueDetails
|
? `${currentProjectDetails.identifier}-${issueDetails.sequence_id}`
|
||||||
? `${currentProjectDetails.identifier}-${issueDetails.sequence_id}`
|
: ""
|
||||||
: ""
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
<CustomHeader.RightItem>
|
||||||
</div>
|
<IssueDetailQuickActions
|
||||||
<IssueDetailQuickActions
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
projectId={projectId.toString()}
|
||||||
projectId={projectId.toString()}
|
issueId={archivedIssueId.toString()}
|
||||||
issueId={archivedIssueId.toString()}
|
/>
|
||||||
/>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const CycleDetailPage = observer(() => {
|
||||||
{cycleId && !isSidebarCollapsed && (
|
{cycleId && !isSidebarCollapsed && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm absolute right-0 top-0 z-[13]"
|
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm absolute right-0 z-[13]"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
boxShadow:
|
boxShadow:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ArrowRight, PanelRight } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
|
|
@ -161,8 +161,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
cycleDetails={cycleDetails ?? undefined}
|
cycleDetails={cycleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-[15] w-full items-center gap-x-2 gap-y-4">
|
<CustomHeader>
|
||||||
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -235,6 +235,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
|
</CustomHeader.LeftItem>
|
||||||
|
<CustomHeader.RightItem>
|
||||||
<div className="hidden items-center gap-2 md:flex ">
|
<div className="hidden items-center gap-2 md:flex ">
|
||||||
<LayoutSelection
|
<LayoutSelection
|
||||||
layouts={[
|
layouts={[
|
||||||
|
|
@ -315,8 +317,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||||
>
|
>
|
||||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { CyclesViewHeader } from "@/components/cycles";
|
import { CyclesViewHeader } from "@/components/cycles";
|
||||||
|
|
@ -30,48 +30,50 @@ export const CyclesListHeader: FC = observer(() => {
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
icon={
|
||||||
icon={
|
currentProjectDetails && (
|
||||||
currentProjectDetails && (
|
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
</span>
|
||||||
</span>
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />}
|
/>
|
||||||
/>
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
<CustomHeader.RightItem>
|
||||||
</div>
|
{canUserCreateCycle && currentProjectDetails ? (
|
||||||
{canUserCreateCycle && currentProjectDetails && (
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<CyclesViewHeader projectId={currentProjectDetails.id} />
|
||||||
<CyclesViewHeader projectId={currentProjectDetails.id} />
|
<Button
|
||||||
<Button
|
variant="primary"
|
||||||
variant="primary"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
onClick={() => {
|
setTrackElement("Cycles page");
|
||||||
setTrackElement("Cycles page");
|
toggleCreateCycleModal(true);
|
||||||
toggleCreateCycleModal(true);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<div className="hidden sm:block">Add</div> Cycle
|
||||||
<div className="hidden sm:block">Add</div> Cycle
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<></>
|
||||||
</div>
|
)}
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
||||||
// types
|
// types
|
||||||
import { TCycleFilters } from "@plane/types";
|
import { TCycleFilters } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
|
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
|
@ -81,13 +82,13 @@ const ProjectCyclesPage = observer(() => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
|
{calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<CycleAppliedFiltersList
|
<CycleAppliedFiltersList
|
||||||
appliedFilters={currentProjectFilters ?? {}}
|
appliedFilters={currentProjectFilters ?? {}}
|
||||||
handleClearAllFilters={() => clearAllFilters(projectId.toString())}
|
handleClearAllFilters={() => clearAllFilters(projectId.toString())}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CyclesView workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
<CyclesView workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, Intake } from "@plane/ui";
|
import { Breadcrumbs, Button, Intake, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
|
import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
|
||||||
|
|
@ -30,8 +30,8 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||||
const isViewer = currentProjectRole === EUserProjectRoles.VIEWER;
|
const isViewer = currentProjectRole === EUserProjectRoles.VIEWER;
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Breadcrumbs isLoading={currentProjectDetailsLoader}>
|
<Breadcrumbs isLoading={currentProjectDetailsLoader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -64,23 +64,26 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
|
<CustomHeader.RightItem>
|
||||||
|
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<InboxIssueCreateEditModalRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
modalState={createIssueModal}
|
||||||
|
handleModalClose={() => setCreateIssueModal(false)}
|
||||||
|
issue={undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer && (
|
<Button variant="primary" size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||||
<div className="flex items-center gap-2">
|
Add issue
|
||||||
<InboxIssueCreateEditModalRoot
|
</Button>
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
</div>
|
||||||
projectId={projectId.toString()}
|
) : (
|
||||||
modalState={createIssueModal}
|
<></>
|
||||||
handleModalClose={() => setCreateIssueModal(false)}
|
)}
|
||||||
issue={undefined}
|
</CustomHeader.RightItem>
|
||||||
/>
|
</CustomHeader>
|
||||||
|
|
||||||
<Button variant="primary" size="sm" onClick={() => setCreateIssueModal(true)}>
|
|
||||||
Add issue
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { PanelRight } from "lucide-react";
|
import { PanelRight } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { IssueDetailQuickActions } from "@/components/issues";
|
import { IssueDetailQuickActions } from "@/components/issues";
|
||||||
|
|
@ -29,8 +29,8 @@ export const ProjectIssueDetailsHeader = observer(() => {
|
||||||
const isSidebarCollapsed = issueDetailSidebarCollapsed;
|
const isSidebarCollapsed = issueDetailSidebarCollapsed;
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -75,17 +75,19 @@ export const ProjectIssueDetailsHeader = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<IssueDetailQuickActions
|
<CustomHeader.RightItem>
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
<IssueDetailQuickActions
|
||||||
projectId={projectId.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
issueId={issueId.toString()}
|
projectId={projectId.toString()}
|
||||||
/>
|
issueId={issueId.toString()}
|
||||||
<button className="block md:hidden" onClick={() => toggleIssueDetailSidebar()}>
|
|
||||||
<PanelRight
|
|
||||||
className={cn("h-4 w-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
<button className="block md:hidden" onClick={() => toggleIssueDetailSidebar()}>
|
||||||
</div>
|
<PanelRight
|
||||||
|
className={cn("h-4 w-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,248 +1,131 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, 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 { Briefcase, Circle, ExternalLink } from "lucide-react";
|
import { Briefcase, Circle, ExternalLink } from "lucide-react";
|
||||||
// types
|
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, Tooltip, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
|
||||||
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
|
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
|
||||||
// constants
|
// constants
|
||||||
import {
|
import HeaderFilters from "@/components/issues/filters";
|
||||||
EIssueFilterType,
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
EIssuesStoreType,
|
|
||||||
EIssueLayoutTypes,
|
|
||||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
|
||||||
} from "@/constants/issue";
|
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
|
||||||
// helpers
|
// helpers
|
||||||
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
|
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import {
|
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||||
useEventTracker,
|
|
||||||
useLabel,
|
|
||||||
useProject,
|
|
||||||
useProjectState,
|
|
||||||
useUser,
|
|
||||||
useMember,
|
|
||||||
useCommandPalette,
|
|
||||||
} from "@/hooks/store";
|
|
||||||
import { useIssues } from "@/hooks/store/use-issues";
|
import { useIssues } from "@/hooks/store/use-issues";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
export const ProjectIssuesHeader = observer(() => {
|
export const ProjectIssuesHeader = observer(() => {
|
||||||
// states
|
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
|
||||||
project: { projectMemberIds },
|
|
||||||
} = useMember();
|
|
||||||
const {
|
|
||||||
issuesFilter: { issueFilters, updateFilters },
|
|
||||||
issues: { getGroupIssueCount },
|
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
|
||||||
const { toggleCreateIssueModal } = useCommandPalette();
|
|
||||||
const { setTrackElement } = useEventTracker();
|
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const {
|
||||||
|
issues: { getGroupIssueCount },
|
||||||
|
} = useIssues(EIssuesStoreType.PROJECT);
|
||||||
|
|
||||||
const { currentProjectDetails, loader } = useProject();
|
const { currentProjectDetails, loader } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
|
||||||
const { projectLabels } = useLabel();
|
const { toggleCreateIssueModal } = useCommandPalette();
|
||||||
|
const { setTrackElement } = useEventTracker();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
|
||||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
// this validation is majorly for the filter start_date, target_date custom
|
|
||||||
value.forEach((val) => {
|
|
||||||
if (!newValues.includes(val)) newValues.push(val);
|
|
||||||
else newValues.splice(newValues.indexOf(val), 1);
|
|
||||||
});
|
|
||||||
} 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 handleLayoutChange = useCallback(
|
|
||||||
(layout: EIssueLayoutTypes) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
|
||||||
},
|
|
||||||
[workspaceSlug, projectId, 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]
|
|
||||||
);
|
|
||||||
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
||||||
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
|
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
|
||||||
|
|
||||||
|
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||||
const canUserCreateIssue =
|
const canUserCreateIssue =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CustomHeader>
|
||||||
<ProjectAnalyticsModal
|
<CustomHeader.LeftItem>
|
||||||
isOpen={analyticsModal}
|
<div className="flex items-center gap-2.5">
|
||||||
onClose={() => setAnalyticsModal(false)}
|
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
|
||||||
projectDetails={currentProjectDetails ?? undefined}
|
<Breadcrumbs.BreadcrumbItem
|
||||||
/>
|
type="text"
|
||||||
|
link={
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<BreadcrumbLink
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
href={`/${workspaceSlug}/projects`}
|
||||||
<div className="flex items-center gap-2.5">
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
|
icon={
|
||||||
<Breadcrumbs.BreadcrumbItem
|
currentProjectDetails ? (
|
||||||
type="text"
|
currentProjectDetails && (
|
||||||
link={
|
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||||
<BreadcrumbLink
|
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||||
href={`/${workspaceSlug}/projects`}
|
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
|
||||||
icon={
|
|
||||||
currentProjectDetails ? (
|
|
||||||
currentProjectDetails && (
|
|
||||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
|
||||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
|
||||||
<Briefcase className="h-4 w-4" />
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
) : (
|
||||||
/>
|
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||||
}
|
<Briefcase className="h-4 w-4" />
|
||||||
/>
|
</span>
|
||||||
|
)
|
||||||
<Breadcrumbs.BreadcrumbItem
|
}
|
||||||
type="text"
|
/>
|
||||||
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />}
|
|
||||||
/>
|
|
||||||
</Breadcrumbs>
|
|
||||||
{issuesCount && issuesCount > 0 ? (
|
|
||||||
<Tooltip
|
|
||||||
isMobile={isMobile}
|
|
||||||
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<CountChip count={issuesCount} />
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{currentProjectDetails?.anchor && (
|
|
||||||
<a
|
|
||||||
href={publishedURL}
|
|
||||||
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
|
|
||||||
Public
|
|
||||||
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="items-center gap-2 hidden md:flex">
|
|
||||||
<LayoutSelection
|
|
||||||
layouts={[
|
|
||||||
EIssueLayoutTypes.LIST,
|
|
||||||
EIssueLayoutTypes.KANBAN,
|
|
||||||
EIssueLayoutTypes.CALENDAR,
|
|
||||||
EIssueLayoutTypes.SPREADSHEET,
|
|
||||||
EIssueLayoutTypes.GANTT,
|
|
||||||
]}
|
|
||||||
onChange={(layout) => handleLayoutChange(layout)}
|
|
||||||
selectedLayout={activeLayout}
|
|
||||||
/>
|
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
|
||||||
<FilterSelection
|
|
||||||
filters={issueFilters?.filters ?? {}}
|
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
|
||||||
layoutDisplayFiltersOptions={
|
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
|
||||||
}
|
}
|
||||||
labels={projectLabels}
|
|
||||||
memberIds={projectMemberIds ?? undefined}
|
|
||||||
states={projectStates}
|
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
|
||||||
<FiltersDropdown title="Display" placement="bottom-end">
|
|
||||||
<DisplayFiltersSelection
|
|
||||||
layoutDisplayFiltersOptions={
|
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
|
||||||
}
|
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
|
||||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
|
||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
|
||||||
/>
|
|
||||||
</FiltersDropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{canUserCreateIssue && (
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<>
|
type="text"
|
||||||
<Button
|
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
className="hidden md:block"
|
/>
|
||||||
onClick={() => setAnalyticsModal(true)}
|
</Breadcrumbs>
|
||||||
variant="neutral-primary"
|
{issuesCount && issuesCount > 0 ? (
|
||||||
size="sm"
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
|
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
|
||||||
|
position="bottom"
|
||||||
>
|
>
|
||||||
Analytics
|
<CountChip count={issuesCount} />
|
||||||
</Button>
|
</Tooltip>
|
||||||
<Button
|
) : null}
|
||||||
onClick={() => {
|
</div>
|
||||||
setTrackElement("Project issues page");
|
{currentProjectDetails?.anchor ? (
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
<a
|
||||||
}}
|
href={publishedURL}
|
||||||
size="sm"
|
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
|
||||||
>
|
target="_blank"
|
||||||
<div className="hidden sm:block">Add</div> Issue
|
rel="noopener noreferrer"
|
||||||
</Button>
|
>
|
||||||
</>
|
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
|
||||||
|
Public
|
||||||
|
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
</>
|
<CustomHeader.RightItem>
|
||||||
|
<div className="hidden gap-3 md:flex">
|
||||||
|
<HeaderFilters
|
||||||
|
projectId={projectId}
|
||||||
|
currentProjectDetails={currentProjectDetails}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
canUserCreateIssue={canUserCreateIssue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{canUserCreateIssue ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("Project issues page");
|
||||||
|
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<div className="hidden sm:block">Add</div> Issue
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const ModuleIssuesPage = observer(() => {
|
||||||
{moduleId && !isSidebarCollapsed && (
|
{moduleId && !isSidebarCollapsed && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm absolute right-0 top-0 z-[13]"
|
"flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 vertical-scrollbar scrollbar-sm absolute right-0 z-[13]"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
boxShadow:
|
boxShadow:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ArrowRight, PanelRight } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
|
|
@ -161,169 +161,164 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
moduleDetails={moduleDetails ?? undefined}
|
moduleDetails={moduleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
|
<CustomHeader>
|
||||||
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
|
<CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<span>
|
||||||
<span>
|
<span className="hidden md:block">
|
||||||
<span className="hidden md:block">
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
|
||||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
|
||||||
icon={
|
|
||||||
currentProjectDetails && (
|
|
||||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
|
||||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<Link
|
|
||||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||||
className="block pl-2 text-custom-text-300 md:hidden"
|
icon={
|
||||||
>
|
currentProjectDetails && (
|
||||||
...
|
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||||
</Link>
|
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
}
|
<Link
|
||||||
/>
|
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||||
<Breadcrumbs.BreadcrumbItem
|
className="block pl-2 text-custom-text-300 md:hidden"
|
||||||
type="text"
|
|
||||||
link={
|
|
||||||
<BreadcrumbLink
|
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/modules`}
|
|
||||||
label="Modules"
|
|
||||||
icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Breadcrumbs.BreadcrumbItem
|
|
||||||
type="component"
|
|
||||||
component={
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
<DiceIcon className="h-3 w-3" />
|
|
||||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
|
||||||
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
|
||||||
{issuesCount && issuesCount > 0 ? (
|
|
||||||
<Tooltip
|
|
||||||
isMobile={isMobile}
|
|
||||||
tooltipContent={`There are ${issuesCount} ${
|
|
||||||
issuesCount > 1 ? "issues" : "issue"
|
|
||||||
} in this module`}
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
|
||||||
{issuesCount}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
className="ml-1.5 flex-shrink-0"
|
|
||||||
placement="bottom-start"
|
|
||||||
>
|
>
|
||||||
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
...
|
||||||
</CustomMenu>
|
</Link>
|
||||||
}
|
</span>
|
||||||
/>
|
}
|
||||||
</Breadcrumbs>
|
/>
|
||||||
</div>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<div className="flex items-center gap-2">
|
type="text"
|
||||||
<div className="hidden gap-2 md:flex">
|
link={
|
||||||
<LayoutSelection
|
<BreadcrumbLink
|
||||||
layouts={[
|
href={`/${workspaceSlug}/projects/${projectId}/modules`}
|
||||||
EIssueLayoutTypes.LIST,
|
label="Modules"
|
||||||
EIssueLayoutTypes.KANBAN,
|
icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />}
|
||||||
EIssueLayoutTypes.CALENDAR,
|
|
||||||
EIssueLayoutTypes.SPREADSHEET,
|
|
||||||
EIssueLayoutTypes.GANTT,
|
|
||||||
]}
|
|
||||||
onChange={(layout) => handleLayoutChange(layout)}
|
|
||||||
selectedLayout={activeLayout}
|
|
||||||
/>
|
|
||||||
<FiltersDropdown
|
|
||||||
title="Filters"
|
|
||||||
placement="bottom-end"
|
|
||||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
|
||||||
>
|
|
||||||
<FilterSelection
|
|
||||||
filters={issueFilters?.filters ?? {}}
|
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
|
||||||
layoutDisplayFiltersOptions={
|
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
|
||||||
}
|
|
||||||
labels={projectLabels}
|
|
||||||
memberIds={projectMemberIds ?? undefined}
|
|
||||||
states={projectStates}
|
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
}
|
||||||
<FiltersDropdown title="Display" placement="bottom-end">
|
/>
|
||||||
<DisplayFiltersSelection
|
<Breadcrumbs.BreadcrumbItem
|
||||||
layoutDisplayFiltersOptions={
|
type="component"
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
component={
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<DiceIcon className="h-3 w-3" />
|
||||||
|
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||||
|
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
||||||
|
{issuesCount && issuesCount > 0 ? (
|
||||||
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
|
tooltipContent={`There are ${issuesCount} ${
|
||||||
|
issuesCount > 1 ? "issues" : "issue"
|
||||||
|
} in this module`}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
|
||||||
|
{issuesCount}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
className="ml-1.5 flex-shrink-0"
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
placement="bottom-start"
|
||||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
|
||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
|
||||||
ignoreGroupedFilters={["module"]}
|
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
|
||||||
/>
|
|
||||||
</FiltersDropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{canUserCreateIssue && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
className="hidden md:block"
|
|
||||||
onClick={() => setAnalyticsModal(true)}
|
|
||||||
variant="neutral-primary"
|
|
||||||
size="sm"
|
|
||||||
>
|
>
|
||||||
Analytics
|
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
||||||
</Button>
|
</CustomMenu>
|
||||||
<Button
|
}
|
||||||
className="hidden sm:flex"
|
/>
|
||||||
onClick={() => {
|
</Breadcrumbs>
|
||||||
setTrackElement("Module issues page");
|
</CustomHeader.LeftItem>
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
<CustomHeader.RightItem>
|
||||||
}}
|
<div className="hidden gap-2 md:flex">
|
||||||
size="sm"
|
<LayoutSelection
|
||||||
>
|
layouts={[
|
||||||
Add issue
|
EIssueLayoutTypes.LIST,
|
||||||
</Button>
|
EIssueLayoutTypes.KANBAN,
|
||||||
</>
|
EIssueLayoutTypes.CALENDAR,
|
||||||
)}
|
EIssueLayoutTypes.SPREADSHEET,
|
||||||
<button
|
EIssueLayoutTypes.GANTT,
|
||||||
type="button"
|
]}
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
onClick={toggleSidebar}
|
selectedLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
<FiltersDropdown
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||||
>
|
>
|
||||||
<ArrowRight
|
<FilterSelection
|
||||||
className={`hidden h-4 w-4 duration-300 md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`}
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
|
layoutDisplayFiltersOptions={
|
||||||
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
|
}
|
||||||
|
labels={projectLabels}
|
||||||
|
memberIds={projectMemberIds ?? undefined}
|
||||||
|
states={projectStates}
|
||||||
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
/>
|
/>
|
||||||
<PanelRight
|
</FiltersDropdown>
|
||||||
className={cn(
|
<FiltersDropdown title="Display" placement="bottom-end">
|
||||||
"block h-4 w-4 md:hidden",
|
<DisplayFiltersSelection
|
||||||
!isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
|
layoutDisplayFiltersOptions={
|
||||||
)}
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
|
}
|
||||||
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
|
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||||
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
|
ignoreGroupedFilters={["module"]}
|
||||||
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
/>
|
/>
|
||||||
</button>
|
</FiltersDropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
{canUserCreateIssue ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="hidden md:block"
|
||||||
|
onClick={() => setAnalyticsModal(true)}
|
||||||
|
variant="neutral-primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Analytics
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="hidden sm:flex"
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("Module issues page");
|
||||||
|
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add issue
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
>
|
||||||
|
<ArrowRight className={`hidden h-4 w-4 duration-300 md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
|
||||||
|
<PanelRight
|
||||||
|
className={cn("block h-4 w-4 md:hidden", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, DiceIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, DiceIcon, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { ModuleViewHeader } from "@/components/modules";
|
import { ModuleViewHeader } from "@/components/modules";
|
||||||
|
|
@ -30,8 +30,8 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -56,10 +56,10 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<CustomHeader.RightItem>
|
||||||
<ModuleViewHeader />
|
<ModuleViewHeader />
|
||||||
{canUserCreateModule && (
|
{canUserCreateModule ? (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -70,8 +70,10 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||||
>
|
>
|
||||||
<div className="hidden sm:block">Add</div> Module
|
<div className="hidden sm:block">Add</div> Module
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,19 +57,17 @@ const ProjectModulesPage = observer(() => {
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
{(calculateTotalFilters(currentProjectFilters ?? {}) !== 0 || currentProjectDisplayFilters?.favorites) && (
|
{(calculateTotalFilters(currentProjectFilters ?? {}) !== 0 || currentProjectDisplayFilters?.favorites) && (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
<ModuleAppliedFiltersList
|
||||||
<ModuleAppliedFiltersList
|
appliedFilters={currentProjectFilters ?? {}}
|
||||||
appliedFilters={currentProjectFilters ?? {}}
|
isFavoriteFilterApplied={currentProjectDisplayFilters?.favorites ?? false}
|
||||||
isFavoriteFilterApplied={currentProjectDisplayFilters?.favorites ?? false}
|
handleClearAllFilters={() => clearAllFilters(`${projectId}`)}
|
||||||
handleClearAllFilters={() => clearAllFilters(`${projectId}`)}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleDisplayFiltersUpdate={(val) => {
|
||||||
handleDisplayFiltersUpdate={(val) => {
|
if (!projectId) return;
|
||||||
if (!projectId) return;
|
updateDisplayFilters(projectId.toString(), val);
|
||||||
updateDisplayFilters(projectId.toString(), val);
|
}}
|
||||||
}}
|
alwaysAllowEditing
|
||||||
alwaysAllowEditing
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<ModulesListView />
|
<ModulesListView />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,15 @@ import { FileText } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TLogoProps } from "@plane/types";
|
import { TLogoProps } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
EmojiIconPicker,
|
||||||
|
EmojiIconPickerTypes,
|
||||||
|
TOAST_TYPE,
|
||||||
|
Tooltip,
|
||||||
|
setToast,
|
||||||
|
CustomHeader,
|
||||||
|
} from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { PageEditInformationPopover } from "@/components/pages";
|
import { PageEditInformationPopover } from "@/components/pages";
|
||||||
|
|
@ -59,8 +67,8 @@ export const PageDetailsHeader = observer(() => {
|
||||||
const pageTitle = getPageName(name);
|
const pageTitle = getPageName(name);
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -158,9 +166,11 @@ export const PageDetailsHeader = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<PageEditInformationPopover page={page} />
|
<CustomHeader.RightItem>
|
||||||
<PageDetailsHeaderExtraActions />
|
<PageEditInformationPopover page={page} />
|
||||||
</div>
|
<PageDetailsHeaderExtraActions />
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { FileText } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomHeader } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -30,8 +30,8 @@ export const PagesListHeader = observer(() => {
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
|
@ -56,24 +56,28 @@ export const PagesListHeader = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
{canUserCreatePage && (
|
<CustomHeader.RightItem>
|
||||||
<div className="flex items-center gap-2">
|
{canUserCreatePage ? (
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="primary"
|
<Button
|
||||||
size="sm"
|
variant="primary"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
setTrackElement("Project pages page");
|
onClick={() => {
|
||||||
toggleCreatePageModal({
|
setTrackElement("Project pages page");
|
||||||
isOpen: true,
|
toggleCreatePageModal({
|
||||||
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
|
isOpen: true,
|
||||||
});
|
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
Add page
|
>
|
||||||
</Button>
|
Add page
|
||||||
</div>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// ui
|
// ui
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -29,8 +29,8 @@ export const ProjectSettingHeader: FC = observer(() => {
|
||||||
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
|
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<div>
|
||||||
<div className="z-50">
|
<div className="z-50">
|
||||||
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
<Breadcrumbs onBack={router.back} isLoading={loader}>
|
||||||
|
|
@ -84,7 +84,7 @@ export const ProjectSettingHeader: FC = observer(() => {
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Layers, Lock } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, Tooltip, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
|
|
@ -136,8 +136,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
const publishLink = getPublishViewLink(viewDetails?.anchor);
|
const publishLink = getPublishViewLink(viewDetails?.anchor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex items-center gap-2">
|
<CustomHeader.LeftItem>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -208,15 +208,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
||||||
{viewDetails?.access === EViewAccess.PRIVATE && (
|
{viewDetails?.access === EViewAccess.PRIVATE ? (
|
||||||
<div className="cursor-default text-custom-text-300">
|
<div className="cursor-default text-custom-text-300">
|
||||||
<Tooltip tooltipContent={"Private"}>
|
<Tooltip tooltipContent={"Private"}>
|
||||||
<Lock className="h-4 w-4" />
|
<Lock className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewDetails?.anchor && publishLink && (
|
{viewDetails?.anchor && publishLink ? (
|
||||||
<a
|
<a
|
||||||
href={publishLink}
|
href={publishLink}
|
||||||
className="px-3 py-1.5 bg-green-500/20 text-green-500 rounded text-xs font-medium flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-green-500/20 text-green-500 rounded text-xs font-medium flex items-center gap-1.5"
|
||||||
|
|
@ -226,10 +228,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
<span className="flex-shrink-0 rounded-full size-1.5 bg-green-500" />
|
<span className="flex-shrink-0 rounded-full size-1.5 bg-green-500" />
|
||||||
Live
|
Live
|
||||||
</a>
|
</a>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
<CustomHeader.RightItem>
|
||||||
{!viewDetails?.is_locked && (
|
{!viewDetails?.is_locked ? (
|
||||||
<>
|
<>
|
||||||
<LayoutSelection
|
<LayoutSelection
|
||||||
layouts={[
|
layouts={[
|
||||||
|
|
@ -278,8 +282,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
{canUserCreateIssue && (
|
{canUserCreateIssue ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||||
|
|
@ -289,8 +295,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
>
|
>
|
||||||
Add issue
|
Add issue
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { Layers } from "lucide-react";
|
import { Layers } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { TViewFilterProps } from "@plane/types";
|
import { Breadcrumbs, Button, CustomHeader } from "@plane/ui";
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { ViewListHeader } from "@/components/views";
|
import { ViewListHeader } from "@/components/views";
|
||||||
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
|
|
||||||
// constants
|
|
||||||
import { EViewAccess } from "@/constants/views";
|
|
||||||
// helpers
|
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useCommandPalette, useProject, useProjectView } from "@/hooks/store";
|
import { useCommandPalette, useProject } from "@/hooks/store";
|
||||||
|
|
||||||
export const ProjectViewsHeader = observer(() => {
|
export const ProjectViewsHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
|
|
@ -24,75 +17,43 @@ export const ProjectViewsHeader = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleCreateViewModal } = useCommandPalette();
|
const { toggleCreateViewModal } = useCommandPalette();
|
||||||
const { currentProjectDetails, loader } = useProject();
|
const { currentProjectDetails, loader } = useProject();
|
||||||
const { filters, updateFilters, clearAllFilters } = useProjectView();
|
|
||||||
|
|
||||||
const handleRemoveFilter = useCallback(
|
|
||||||
(key: keyof TViewFilterProps, value: string | EViewAccess | null) => {
|
|
||||||
let newValues = filters.filters?.[key];
|
|
||||||
|
|
||||||
if (key === "favorites") {
|
|
||||||
newValues = !!value;
|
|
||||||
}
|
|
||||||
if (Array.isArray(newValues)) {
|
|
||||||
if (!value) newValues = [];
|
|
||||||
else newValues = newValues.filter((val) => val !== value) as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFilters("filters", { [key]: newValues });
|
|
||||||
},
|
|
||||||
[filters.filters, updateFilters]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
icon={
|
||||||
icon={
|
currentProjectDetails && (
|
||||||
currentProjectDetails && (
|
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
||||||
<Logo logo={currentProjectDetails?.logo_props} size={16} />
|
</span>
|
||||||
</span>
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
/>
|
||||||
/>
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
<CustomHeader.RightItem>
|
||||||
</div>
|
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
|
||||||
<ViewListHeader />
|
<ViewListHeader />
|
||||||
<div>
|
<div>
|
||||||
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
|
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
|
||||||
Add view
|
Add view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
{isFiltersApplied && (
|
|
||||||
<div className="border-t border-custom-border-200 px-5 py-3">
|
|
||||||
<ViewAppliedFiltersList
|
|
||||||
appliedFilters={filters.filters ?? {}}
|
|
||||||
handleClearAllFilters={clearAllFilters}
|
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
|
||||||
alwaysAllowEditing
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// components
|
// components
|
||||||
|
import { TViewFilterProps } from "@plane/types";
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { ProjectViewsList } from "@/components/views";
|
import { ProjectViewsList } from "@/components/views";
|
||||||
// constants
|
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
|
// constants
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject, useProjectView } from "@/hooks/store";
|
||||||
|
|
||||||
const ProjectViewsPage = observer(() => {
|
const ProjectViewsPage = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
// store
|
// store
|
||||||
const { getProjectById, currentProjectDetails } = useProject();
|
const { getProjectById, currentProjectDetails } = useProject();
|
||||||
|
const { filters, updateFilters, clearAllFilters } = useProjectView();
|
||||||
// derived values
|
// derived values
|
||||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||||
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
|
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
|
||||||
|
|
@ -32,10 +40,38 @@ const ProjectViewsPage = observer(() => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
const handleRemoveFilter = useCallback(
|
||||||
|
(key: keyof TViewFilterProps, value: string | EViewAccess | null) => {
|
||||||
|
let newValues = filters.filters?.[key];
|
||||||
|
|
||||||
|
if (key === "favorites") {
|
||||||
|
newValues = !!value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(newValues)) {
|
||||||
|
if (!value) newValues = [];
|
||||||
|
else newValues = newValues.filter((val) => val !== value) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters("filters", { [key]: newValues });
|
||||||
|
},
|
||||||
|
[filters.filters, updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
|
{isFiltersApplied && (
|
||||||
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
|
<ViewAppliedFiltersList
|
||||||
|
appliedFilters={filters.filters ?? {}}
|
||||||
|
handleClearAllFilters={clearAllFilters}
|
||||||
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
alwaysAllowEditing
|
||||||
|
/>
|
||||||
|
</CustomHeader>
|
||||||
|
)}
|
||||||
<ProjectViewsList />
|
<ProjectViewsList />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -14,24 +14,22 @@ export const WorkspaceSettingHeader: FC = observer(() => {
|
||||||
const { currentWorkspace, loader } = useWorkspace();
|
const { currentWorkspace, loader } = useWorkspace();
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<Breadcrumbs isLoading={loader}>
|
||||||
<Breadcrumbs isLoading={loader}>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={
|
||||||
link={
|
<BreadcrumbLink
|
||||||
<BreadcrumbLink
|
href={`/${currentWorkspace?.slug}/settings`}
|
||||||
href={`/${currentWorkspace?.slug}/settings`}
|
label={currentWorkspace?.name ?? "Workspace"}
|
||||||
label={currentWorkspace?.name ?? "Workspace"}
|
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Settings" />} />
|
||||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Settings" />} />
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useParams } from "next/navigation";
|
||||||
import { Layers } from "lucide-react";
|
import { Layers } from "lucide-react";
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
|
||||||
|
|
@ -98,17 +98,18 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="relative flex gap-2">
|
<CustomHeader.LeftItem>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={<BreadcrumbLink label={`Views`} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
link={<BreadcrumbLink label={`Views`} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{!isLocked && (
|
<CustomHeader.RightItem>
|
||||||
|
{!isLocked ? (
|
||||||
<>
|
<>
|
||||||
<FiltersDropdown
|
<FiltersDropdown
|
||||||
title="Filters"
|
title="Filters"
|
||||||
|
|
@ -135,13 +136,15 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
||||||
Add view
|
Add view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||||
href={href}
|
href={href}
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
<div className="flex h-5 w-5 items-center justify-start overflow-hidden !text-[1rem]">{icon}</div>
|
||||||
)}
|
)}
|
||||||
{label && (
|
{label && (
|
||||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
// components
|
// components
|
||||||
|
import { CustomRow } from "@plane/ui";
|
||||||
import { SidebarHamburgerToggle } from "@/components/core";
|
import { SidebarHamburgerToggle } from "@/components/core";
|
||||||
|
|
||||||
export interface AppHeaderProps {
|
export interface AppHeaderProps {
|
||||||
|
|
@ -13,16 +14,14 @@ export const AppHeader = (props: AppHeaderProps) => {
|
||||||
const { header, mobileHeader } = props;
|
const { header, mobileHeader } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="z-[15]">
|
||||||
<div className="z-[15]">
|
<CustomRow className="h-[3.75rem] z-10 flex gap-2 w-full items-center border-b border-custom-border-200">
|
||||||
<div className="z-10 flex w-full items-center border-b border-custom-border-200">
|
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||||
<div className="block bg-custom-sidebar-background-100 py-4 pl-5 md:hidden">
|
<SidebarHamburgerToggle />
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
</div>
|
|
||||||
<div className="w-full">{header}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{mobileHeader && mobileHeader}
|
<div className="w-full">{header}</div>
|
||||||
</div>
|
</CustomRow>
|
||||||
</>
|
{mobileHeader && mobileHeader}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
MoveRight,
|
MoveRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomHeader, CustomMenu, EHeaderVariant } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { InboxIssueStatus } from "@/components/inbox";
|
import { InboxIssueStatus } from "@/components/inbox";
|
||||||
import { IssueUpdateStatus } from "@/components/issues";
|
import { IssueUpdateStatus } from "@/components/issues";
|
||||||
|
|
@ -80,7 +80,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
if (!issue || !inboxIssue) return null;
|
if (!issue || !inboxIssue) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-12 relative flex border-custom-border-200 w-full items-center gap-2 px-4">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||||
{isNotificationEmbed && (
|
{isNotificationEmbed && (
|
||||||
<button onClick={embedRemoveCurrentNotification}>
|
<button onClick={embedRemoveCurrentNotification}>
|
||||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
|
|
@ -89,7 +89,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
<PanelLeft
|
<PanelLeft
|
||||||
onClick={() => setIsMobileSidebar(!isMobileSidebar)}
|
onClick={() => setIsMobileSidebar(!isMobileSidebar)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-4 h-4 flex-shrink-0 mr-2",
|
"w-4 h-4 flex-shrink-0 mr-2 my-auto",
|
||||||
isMobileSidebar ? "text-custom-primary-100" : "text-custom-text-200"
|
isMobileSidebar ? "text-custom-primary-100" : "text-custom-text-200"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -181,6 +181,6 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import {
|
import {
|
||||||
InboxIssueAppliedFiltersStatus,
|
InboxIssueAppliedFiltersStatus,
|
||||||
InboxIssueAppliedFiltersPriority,
|
InboxIssueAppliedFiltersPriority,
|
||||||
|
|
@ -17,7 +18,7 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
||||||
|
|
||||||
if (getAppliedFiltersCount === 0) return <></>;
|
if (getAppliedFiltersCount === 0) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="p-3 py-2 relative flex flex-wrap items-center gap-1 border-b border-custom-border-300">
|
<CustomHeader variant={EHeaderVariant.TERNARY} className="flex-wrap items-center gap-1 min-h-none">
|
||||||
{/* status */}
|
{/* status */}
|
||||||
<InboxIssueAppliedFiltersStatus />
|
<InboxIssueAppliedFiltersStatus />
|
||||||
{/* state */}
|
{/* state */}
|
||||||
|
|
@ -34,6 +35,6 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
||||||
<InboxIssueAppliedFiltersDate filterKey="created_at" label="Created date" />
|
<InboxIssueAppliedFiltersDate filterKey="created_at" label="Created date" />
|
||||||
{/* updated_at */}
|
{/* updated_at */}
|
||||||
<InboxIssueAppliedFiltersDate filterKey="updated_at" label="Updated date" />
|
<InboxIssueAppliedFiltersDate filterKey="updated_at" label="Updated date" />
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,33 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { ListFilter } from "lucide-react";
|
import { ChevronDown, ListFilter } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
|
import { cn } from "@plane/editor";
|
||||||
|
import { getButtonStyling } from "@plane/ui";
|
||||||
import { InboxIssueFilterSelection, InboxIssueOrderByDropdown } from "@/components/inbox/inbox-filter";
|
import { InboxIssueFilterSelection, InboxIssueOrderByDropdown } from "@/components/inbox/inbox-filter";
|
||||||
import { FiltersDropdown } from "@/components/issues";
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
|
|
||||||
|
const smallButton = <ListFilter className="h-3 " />;
|
||||||
|
const largeButton = (
|
||||||
|
<div className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}>
|
||||||
|
<ListFilter className="h-3 " />
|
||||||
|
<span className="hidden lg:flex">Filters</span>
|
||||||
|
|
||||||
|
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
export const FiltersRoot: FC = () => (
|
export const FiltersRoot: FC = () => (
|
||||||
<div className="relative flex items-center gap-2">
|
<div className="relative flex items-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
menuButton={
|
||||||
|
<>
|
||||||
|
<div className="hidden 2xl:flex">{largeButton}</div>
|
||||||
|
<div className="flex 2xl:hidden">{smallButton}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
title=""
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
<InboxIssueFilterSelection />
|
<InboxIssueFilterSelection />
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,27 @@ export const InboxIssueOrderByDropdown: FC = observer(() => {
|
||||||
const { inboxSorting, handleInboxIssueSorting } = useProjectInbox();
|
const { inboxSorting, handleInboxIssueSorting } = useProjectInbox();
|
||||||
const orderByDetails =
|
const orderByDetails =
|
||||||
INBOX_ISSUE_ORDER_BY_OPTIONS.find((option) => inboxSorting?.order_by?.includes(option.key)) || undefined;
|
INBOX_ISSUE_ORDER_BY_OPTIONS.find((option) => inboxSorting?.order_by?.includes(option.key)) || undefined;
|
||||||
|
const smallButton =
|
||||||
|
inboxSorting?.sort_by === "asc" ? <ArrowUpWideNarrow className="h-3 " /> : <ArrowDownWideNarrow className="h-3 " />;
|
||||||
|
const largeButton = (
|
||||||
|
<div className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}>
|
||||||
|
{inboxSorting?.sort_by === "asc" ? (
|
||||||
|
<ArrowUpWideNarrow className="h-3 " />
|
||||||
|
) : (
|
||||||
|
<ArrowDownWideNarrow className="h-3 " />
|
||||||
|
)}
|
||||||
|
{orderByDetails?.label || "Order By"}
|
||||||
|
|
||||||
|
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300")}>
|
<>
|
||||||
{inboxSorting?.sort_by === "asc" ? (
|
<div className="hidden 2xl:flex">{largeButton}</div>
|
||||||
<ArrowUpWideNarrow className="h-3 w-3" />
|
<div className="flex 2xl:hidden">{smallButton}</div>
|
||||||
) : (
|
</>
|
||||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
{orderByDetails?.label || "Order By"}
|
|
||||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
maxHeight="lg"
|
maxHeight="lg"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { TInboxIssueCurrentTab } from "@plane/types";
|
import { TInboxIssueCurrentTab } from "@plane/types";
|
||||||
import { Loader } from "@plane/ui";
|
import { CustomHeader, Loader, EHeaderVariant } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox";
|
import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox";
|
||||||
|
|
@ -76,7 +76,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-custom-background-100 flex-shrink-0 w-full h-full border-r border-custom-border-300 ">
|
<div className="bg-custom-background-100 flex-shrink-0 w-full h-full border-r border-custom-border-300 ">
|
||||||
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||||
<div className="border-b border-custom-border-300 flex-shrink-0 w-full h-[50px] relative flex items-center gap-2 whitespace-nowrap px-3">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||||
{tabNavigationOptions.map((option) => (
|
{tabNavigationOptions.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option?.key}
|
key={option?.key}
|
||||||
|
|
@ -105,11 +105,10 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="ml-auto">
|
<div className="m-auto mr-0">
|
||||||
<FiltersRoot />
|
<FiltersRoot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader>
|
||||||
|
|
||||||
<InboxIssueAppliedFilters />
|
<InboxIssueAppliedFilters />
|
||||||
|
|
||||||
{loader != undefined && loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
|
{loader != undefined && loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
|
||||||
|
|
|
||||||
143
web/core/components/issues/filters.tsx
Normal file
143
web/core/components/issues/filters.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { TProject } from "ee/types";
|
||||||
|
// types
|
||||||
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
|
||||||
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
|
// constants
|
||||||
|
import {
|
||||||
|
EIssueFilterType,
|
||||||
|
EIssuesStoreType,
|
||||||
|
EIssueLayoutTypes,
|
||||||
|
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||||
|
} from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
|
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
|
||||||
|
import { ProjectAnalyticsModal } from "../analytics";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentProjectDetails: TProject | undefined;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
canUserCreateIssue: boolean | undefined;
|
||||||
|
};
|
||||||
|
const HeaderFilters = ({ currentProjectDetails, projectId, workspaceSlug, canUserCreateIssue }: Props) => {
|
||||||
|
// states
|
||||||
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
project: { projectMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
const {
|
||||||
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
|
} = useIssues(EIssuesStoreType.PROJECT);
|
||||||
|
|
||||||
|
const { projectStates } = useProjectState();
|
||||||
|
const { projectLabels } = useLabel();
|
||||||
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
|
const handleFiltersUpdate = useCallback(
|
||||||
|
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// this validation is majorly for the filter start_date, target_date custom
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
else newValues.splice(newValues.indexOf(val), 1);
|
||||||
|
});
|
||||||
|
} 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 handleLayoutChange = useCallback(
|
||||||
|
(layout: EIssueLayoutTypes) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, 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}
|
||||||
|
/>
|
||||||
|
<LayoutSelection
|
||||||
|
layouts={[
|
||||||
|
EIssueLayoutTypes.LIST,
|
||||||
|
EIssueLayoutTypes.KANBAN,
|
||||||
|
EIssueLayoutTypes.CALENDAR,
|
||||||
|
EIssueLayoutTypes.SPREADSHEET,
|
||||||
|
EIssueLayoutTypes.GANTT,
|
||||||
|
]}
|
||||||
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
|
selectedLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||||
|
<FilterSelection
|
||||||
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
|
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||||
|
labels={projectLabels}
|
||||||
|
memberIds={projectMemberIds ?? undefined}
|
||||||
|
states={projectStates}
|
||||||
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
<FiltersDropdown title="Display" placement="bottom-end">
|
||||||
|
<DisplayFiltersSelection
|
||||||
|
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||||
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
|
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||||
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
{canUserCreateIssue ? (
|
||||||
|
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
|
Analytics
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderFilters;
|
||||||
|
|
@ -59,7 +59,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||||
!disableEditing && (alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER));
|
!disableEditing && (alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate">
|
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate my-auto">
|
||||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||||
const filterKey = key as keyof IIssueFilterOptions;
|
const filterKey = key as keyof IIssueFilterOptions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
||||||
|
|
@ -76,15 +77,16 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId || !cycleId) return null;
|
if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId || !cycleId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between p-4 gap-2.5">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<AppliedFiltersList
|
<CustomHeader.LeftItem>
|
||||||
appliedFilters={appliedFilters}
|
<AppliedFiltersList
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
labels={projectLabels ?? []}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
states={projectStates}
|
labels={projectLabels ?? []}
|
||||||
/>
|
states={projectStates}
|
||||||
|
/>
|
||||||
|
</CustomHeader.LeftItem>
|
||||||
<SaveFilterView
|
<SaveFilterView
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
|
|
@ -94,6 +96,6 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
display_properties: issueFilters?.displayProperties,
|
display_properties: issueFilters?.displayProperties,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import isEmpty from "lodash/isEmpty";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// types
|
// types
|
||||||
import { cn } from "@plane/editor";
|
|
||||||
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||||
//ui
|
//ui
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedFiltersList } from "@/components/issues";
|
import { AppliedFiltersList } from "@/components/issues";
|
||||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||||
|
|
@ -133,7 +133,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||||
if (areAppliedFiltersEmpty && areFiltersEqual) return null;
|
if (areAppliedFiltersEmpty && areFiltersEqual) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<CreateUpdateWorkspaceViewModal
|
<CreateUpdateWorkspaceViewModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
|
@ -144,31 +144,28 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||||
...viewFilters,
|
...viewFilters,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className={cn("flex items-start justify-between gap-4 p-4", {
|
|
||||||
"justify-end": areAppliedFiltersEmpty,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AppliedFiltersList
|
|
||||||
labels={workspaceLabels ?? undefined}
|
|
||||||
appliedFilters={appliedFilters ?? {}}
|
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
|
||||||
disableEditing={isLocked}
|
|
||||||
alwaysAllowEditing
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isDefaultView && (
|
<AppliedFiltersList
|
||||||
<UpdateViewComponent
|
labels={workspaceLabels ?? undefined}
|
||||||
isLocked={isLocked}
|
appliedFilters={appliedFilters ?? {}}
|
||||||
areFiltersEqual={!!areFiltersEqual}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
isOwner={isOwner}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
isAuthorizedUser={isAuthorizedUser}
|
disableEditing={isLocked}
|
||||||
setIsModalOpen={setIsModalOpen}
|
alwaysAllowEditing
|
||||||
handleUpdateView={handleUpdateView}
|
/>
|
||||||
/>
|
|
||||||
)}
|
{!isDefaultView ? (
|
||||||
</div>
|
<UpdateViewComponent
|
||||||
</>
|
isLocked={isLocked}
|
||||||
|
areFiltersEqual={!!areFiltersEqual}
|
||||||
|
isOwner={isOwner}
|
||||||
|
isAuthorizedUser={isAuthorizedUser}
|
||||||
|
setIsModalOpen={setIsModalOpen}
|
||||||
|
handleUpdateView={handleUpdateView}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
||||||
|
|
@ -75,15 +76,16 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId || Object.keys(appliedFilters).length === 0) return null;
|
if (!workspaceSlug || !projectId || !moduleId || Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between p-4 gap-2.5">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<AppliedFiltersList
|
<CustomHeader.LeftItem>
|
||||||
appliedFilters={appliedFilters}
|
<AppliedFiltersList
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
labels={projectLabels ?? []}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
states={projectStates}
|
labels={projectLabels ?? []}
|
||||||
/>
|
states={projectStates}
|
||||||
|
/>
|
||||||
|
</CustomHeader.LeftItem>
|
||||||
<SaveFilterView
|
<SaveFilterView
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
|
|
@ -93,6 +95,6 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
display_properties: issueFilters?.displayProperties,
|
display_properties: issueFilters?.displayProperties,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
|
|
@ -67,7 +68,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between p-4 gap-2.5">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
|
|
@ -86,6 +87,6 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useParams } from "next/navigation";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedFiltersList } from "@/components/issues";
|
import { AppliedFiltersList } from "@/components/issues";
|
||||||
import { CreateUpdateProjectViewModal } from "@/components/views";
|
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||||
|
|
@ -113,7 +114,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
const isOwner = viewDetails?.owned_by === data?.id;
|
const isOwner = viewDetails?.owned_by === data?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-4 p-4">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<CreateUpdateProjectViewModal
|
<CreateUpdateProjectViewModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
|
@ -127,7 +128,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
...viewFilters,
|
...viewFilters,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<CustomHeader.LeftItem>
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters ?? {}}
|
appliedFilters={appliedFilters ?? {}}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
|
|
@ -136,7 +137,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
states={projectStates}
|
states={projectStates}
|
||||||
disableEditing={isLocked}
|
disableEditing={isLocked}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
<UpdateViewComponent
|
<UpdateViewComponent
|
||||||
isLocked={isLocked}
|
isLocked={isLocked}
|
||||||
areFiltersEqual={!!areFiltersEqual}
|
areFiltersEqual={!!areFiltersEqual}
|
||||||
|
|
@ -145,6 +146,6 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
handleUpdateView={handleUpdateView}
|
handleUpdateView={handleUpdateView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||||
|
|
@ -36,91 +37,93 @@ export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
const isEditingAllowed = alwaysAllowEditing;
|
const isEditingAllowed = alwaysAllowEditing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
<CustomHeader variant={EHeaderVariant.TERNARY} className="flex flex-wrap gap-2">
|
||||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
<CustomHeader.LeftItem>
|
||||||
const filterKey = key as keyof TModuleFilters;
|
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||||
|
const filterKey = key as keyof TModuleFilters;
|
||||||
|
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
if (Array.isArray(value) && value.length === 0) return;
|
if (Array.isArray(value) && value.length === 0) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
key={filterKey}
|
||||||
|
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 capitalize"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||||
|
{filterKey === "status" && (
|
||||||
|
<AppliedStatusFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter("status", val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{DATE_FILTERS.includes(filterKey) && (
|
||||||
|
<AppliedDateFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||||
|
<AppliedMembersFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEditingAllowed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||||
|
>
|
||||||
|
<X size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isArchived && isFavoriteFilterApplied && (
|
||||||
<div
|
<div
|
||||||
key={filterKey}
|
key="module_display_filters"
|
||||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
<span className="text-xs text-custom-text-300">Modules</span>
|
||||||
{filterKey === "status" && (
|
<div className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
||||||
<AppliedStatusFilters
|
Favorite
|
||||||
editable={isEditingAllowed}
|
{isEditingAllowed && (
|
||||||
handleRemove={(val) => handleRemoveFilter("status", val)}
|
<button
|
||||||
values={value}
|
type="button"
|
||||||
/>
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
)}
|
onClick={() =>
|
||||||
{DATE_FILTERS.includes(filterKey) && (
|
handleDisplayFiltersUpdate &&
|
||||||
<AppliedDateFilters
|
handleDisplayFiltersUpdate({
|
||||||
editable={isEditingAllowed}
|
favorites: !isFavoriteFilterApplied,
|
||||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
})
|
||||||
values={value}
|
}
|
||||||
/>
|
>
|
||||||
)}
|
<X size={10} strokeWidth={2} />
|
||||||
{MEMBERS_FILTERS.includes(filterKey) && (
|
</button>
|
||||||
<AppliedMembersFilters
|
)}
|
||||||
editable={isEditingAllowed}
|
</div>
|
||||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
|
||||||
values={value}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isEditingAllowed && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
|
||||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
|
||||||
>
|
|
||||||
<X size={12} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
{isEditingAllowed && (
|
||||||
{!isArchived && isFavoriteFilterApplied && (
|
<button
|
||||||
<div
|
type="button"
|
||||||
key="module_display_filters"
|
onClick={handleClearAllFilters}
|
||||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
Clear all
|
||||||
<span className="text-xs text-custom-text-300">Modules</span>
|
<X size={12} strokeWidth={2} />
|
||||||
<div className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
</button>
|
||||||
Favorite
|
)}
|
||||||
{isEditingAllowed && (
|
</CustomHeader.LeftItem>
|
||||||
<button
|
</CustomHeader>
|
||||||
type="button"
|
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
|
||||||
onClick={() =>
|
|
||||||
handleDisplayFiltersUpdate &&
|
|
||||||
handleDisplayFiltersUpdate({
|
|
||||||
favorites: !isFavoriteFilterApplied,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<X size={10} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isEditingAllowed && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClearAllFilters}
|
|
||||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
|
||||||
>
|
|
||||||
Clear all
|
|
||||||
<X size={12} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-grow items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
{is_locked && <LockedComponent />}
|
{is_locked && <LockedComponent />}
|
||||||
{archived_at && (
|
{archived_at && (
|
||||||
<div className="flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
<div className="flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
|
|
@ -42,8 +43,8 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center border-b border-custom-border-200 px-2 py-1">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex justify-between">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0 my-auto">
|
||||||
<PageSummaryPopover
|
<PageSummaryPopover
|
||||||
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
||||||
isFullWidth={isFullWidth}
|
isFullWidth={isFullWidth}
|
||||||
|
|
@ -59,12 +60,12 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
page={page}
|
page={page}
|
||||||
readOnlyEditorRef={readOnlyEditorRef}
|
readOnlyEditorRef={readOnlyEditorRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
<div className="border-b border-custom-border-200 py-1 px-2">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
{(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && (
|
{(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && (
|
||||||
<PageToolbar editorRef={editorRef?.current} />
|
<PageToolbar editorRef={editorRef?.current} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -44,13 +45,8 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="hidden md:flex items-center border-b border-custom-border-200 px-3 py-2 md:px-5">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="hidden md:flex justify-between">
|
||||||
<div
|
<div className={cn("flex-shrink-0 my-auto")}>
|
||||||
className={cn("flex-shrink-0", {
|
|
||||||
"w-40 lg:w-56": !isFullWidth,
|
|
||||||
"w-[5%]": isFullWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PageSummaryPopover
|
<PageSummaryPopover
|
||||||
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
||||||
isFullWidth={isFullWidth}
|
isFullWidth={isFullWidth}
|
||||||
|
|
@ -69,7 +65,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
page={page}
|
page={page}
|
||||||
readOnlyEditorRef={readOnlyEditorRef}
|
readOnlyEditorRef={readOnlyEditorRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<PageEditorMobileHeaderRoot
|
<PageEditorMobileHeaderRoot
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
||||||
import { ListFilter } from "lucide-react";
|
import { ListFilter } from "lucide-react";
|
||||||
import { TPageFilterProps, TPageNavigationTabs } from "@plane/types";
|
import { TPageFilterProps, TPageNavigationTabs } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { FiltersDropdown } from "@/components/issues";
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
import {
|
import {
|
||||||
PageAppliedFiltersList,
|
PageAppliedFiltersList,
|
||||||
|
|
@ -49,9 +50,11 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex-shrink-0 h-[50px] w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||||
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
|
<CustomHeader.LeftItem>
|
||||||
<div className="h-full flex items-center gap-2 self-end">
|
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
|
||||||
|
</CustomHeader.LeftItem>
|
||||||
|
<CustomHeader.RightItem>
|
||||||
<PageSearchInput
|
<PageSearchInput
|
||||||
searchQuery={filters.searchQuery}
|
searchQuery={filters.searchQuery}
|
||||||
updateSearchQuery={(val) => updateFilters("searchQuery", val)}
|
updateSearchQuery={(val) => updateFilters("searchQuery", val)}
|
||||||
|
|
@ -76,17 +79,17 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
memberIds={workspaceMemberIds ?? undefined}
|
memberIds={workspaceMemberIds ?? undefined}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
{calculateTotalFilters(filters?.filters ?? {}) !== 0 && (
|
{calculateTotalFilters(filters?.filters ?? {}) !== 0 && (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<PageAppliedFiltersList
|
<PageAppliedFiltersList
|
||||||
appliedFilters={filters.filters ?? {}}
|
appliedFilters={filters.filters ?? {}}
|
||||||
handleClearAllFilters={clearAllFilters}
|
handleClearAllFilters={clearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
alwaysAllowEditing
|
alwaysAllowEditing
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ export const PageSearchInput: FC<Props> = (props) => {
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex">
|
||||||
{!isSearchOpen && (
|
{!isSearchOpen && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex-shrink-0 hover:bg-custom-background-80 rounded text-custom-text-400 relative flex justify-center items-center w-6 h-6"
|
className="flex-shrink-0 hover:bg-custom-background-80 rounded text-custom-text-400 relative flex justify-center items-center w-6 h-6 my-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsSearchOpen(true);
|
setIsSearchOpen(true);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
|
|
@ -49,10 +49,9 @@ export const PageSearchInput: FC<Props> = (props) => {
|
||||||
<Search className="h-3.5 w-3.5" />
|
<Search className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
|
"flex items-center justify-start rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
|
||||||
{
|
{
|
||||||
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
|
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +79,6 @@ export const PageSearchInput: FC<Props> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
const isEditingAllowed = alwaysAllowEditing;
|
const isEditingAllowed = alwaysAllowEditing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start justify-between gap-1.5">
|
<div className="flex items-start justify-between gap-1.5 my-auto w-full">
|
||||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||||
{/* Applied filters */}
|
{/* Applied filters */}
|
||||||
{Object.entries(appliedFilters ?? {}).map(([key, value]) => {
|
{Object.entries(appliedFilters ?? {}).map(([key, value]) => {
|
||||||
|
|
@ -132,7 +132,7 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="bg-custom-background-80 rounded-full text-sm font-medium py-1 px-2.5">
|
<span className="bg-custom-background-80 rounded-full text-sm font-medium py-1 px-2.5 my-auto">
|
||||||
{filteredProjects}/{totalProjects}
|
{filteredProjects}/{totalProjects}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
93
web/core/components/project/filters.tsx
Normal file
93
web/core/components/project/filters.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { ListFilter } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { cn } from "@plane/editor";
|
||||||
|
import { TProjectFilters } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
|
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
|
import { useMember, useProjectFilter } from "@/hooks/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filterMenuButton?: React.ReactNode;
|
||||||
|
classname?: string;
|
||||||
|
filterClassname?: string;
|
||||||
|
isMobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderFilters = ({ filterMenuButton, isMobile, classname = "", filterClassname = "" }: Props) => {
|
||||||
|
// router
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
|
const {
|
||||||
|
currentWorkspaceDisplayFilters: displayFilters,
|
||||||
|
currentWorkspaceFilters: filters,
|
||||||
|
updateFilters,
|
||||||
|
updateDisplayFilters,
|
||||||
|
} = useProjectFilter();
|
||||||
|
const {
|
||||||
|
workspace: { workspaceMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
const handleFilters = useCallback(
|
||||||
|
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
let newValues = filters?.[key] ?? [];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
else newValues.splice(newValues.indexOf(val), 1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else {
|
||||||
|
if (key === "created_at") newValues = [value];
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters(workspaceSlug.toString(), { [key]: newValues });
|
||||||
|
},
|
||||||
|
[filters, updateFilters, workspaceSlug]
|
||||||
|
);
|
||||||
|
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex gap-3", classname)}>
|
||||||
|
<ProjectOrderByDropdown
|
||||||
|
value={displayFilters?.order_by}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
||||||
|
updateDisplayFilters(workspaceSlug.toString(), {
|
||||||
|
order_by: val,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isMobile={isMobile}
|
||||||
|
/>
|
||||||
|
<div className={cn(filterClassname)}>
|
||||||
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
menuButton={filterMenuButton || null}
|
||||||
|
>
|
||||||
|
<ProjectFiltersSelection
|
||||||
|
displayFilters={displayFilters ?? {}}
|
||||||
|
filters={filters ?? {}}
|
||||||
|
handleFiltersUpdate={handleFilters}
|
||||||
|
handleDisplayFiltersUpdate={(val) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
updateDisplayFilters(workspaceSlug.toString(), val);
|
||||||
|
}}
|
||||||
|
memberIds={workspaceMemberIds ?? undefined}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default HeaderFilters;
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Search, Briefcase, X, ListFilter } from "lucide-react";
|
import { Search, Briefcase, X } from "lucide-react";
|
||||||
// types
|
|
||||||
import { TProjectFilters } from "@plane/types";
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { FiltersDropdown } from "@/components/issues";
|
|
||||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
import { useCommandPalette, useEventTracker, useProjectFilter, useUser } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
|
import HeaderFilters from "./filters";
|
||||||
|
|
||||||
export const ProjectsBaseHeader = observer(() => {
|
export const ProjectsBaseHeader = observer(() => {
|
||||||
// router
|
|
||||||
const { workspaceSlug } = useParams();
|
|
||||||
// states
|
// states
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
|
|
@ -36,17 +30,8 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const { searchQuery, updateSearchQuery } = useProjectFilter();
|
||||||
currentWorkspaceDisplayFilters: displayFilters,
|
|
||||||
currentWorkspaceFilters: filters,
|
|
||||||
updateFilters,
|
|
||||||
updateDisplayFilters,
|
|
||||||
searchQuery,
|
|
||||||
updateSearchQuery,
|
|
||||||
} = useProjectFilter();
|
|
||||||
const {
|
|
||||||
workspace: { workspaceMemberIds },
|
|
||||||
} = useMember();
|
|
||||||
// outside click detector hook
|
// outside click detector hook
|
||||||
useOutsideClickDetector(inputRef, () => {
|
useOutsideClickDetector(inputRef, () => {
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
|
|
@ -55,29 +40,6 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
const isArchived = pathname.includes("/archives");
|
const isArchived = pathname.includes("/archives");
|
||||||
|
|
||||||
const handleFilters = useCallback(
|
|
||||||
(key: keyof TProjectFilters, value: string | string[]) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
let newValues = filters?.[key] ?? [];
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
|
|
||||||
value.forEach((val) => {
|
|
||||||
if (!newValues.includes(val)) newValues.push(val);
|
|
||||||
else newValues.splice(newValues.indexOf(val), 1);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
|
||||||
else {
|
|
||||||
if (key === "created_at") newValues = [value];
|
|
||||||
else newValues.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFilters(workspaceSlug.toString(), { [key]: newValues });
|
|
||||||
},
|
|
||||||
[filters, updateFilters, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||||
|
|
@ -89,22 +51,18 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
if (searchQuery.trim() !== "") setIsSearchOpen(true);
|
if (searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
|
||||||
|
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader>
|
||||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs.BreadcrumbItem
|
||||||
<Breadcrumbs.BreadcrumbItem
|
type="text"
|
||||||
type="text"
|
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
/>
|
||||||
/>
|
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
|
||||||
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
</CustomHeader.LeftItem>
|
||||||
</div>
|
<CustomHeader.RightItem>
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center justify-end gap-3">
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!isSearchOpen && (
|
{!isSearchOpen && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -149,35 +107,11 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:flex gap-3">
|
|
||||||
<ProjectOrderByDropdown
|
<div className="hidden md:flex">
|
||||||
value={displayFilters?.order_by}
|
<HeaderFilters />
|
||||||
onChange={(val) => {
|
|
||||||
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
|
||||||
updateDisplayFilters(workspaceSlug.toString(), {
|
|
||||||
order_by: val,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FiltersDropdown
|
|
||||||
icon={<ListFilter className="h-3 w-3" />}
|
|
||||||
title="Filters"
|
|
||||||
placement="bottom-end"
|
|
||||||
isFiltersApplied={isFiltersApplied}
|
|
||||||
>
|
|
||||||
<ProjectFiltersSelection
|
|
||||||
displayFilters={displayFilters ?? {}}
|
|
||||||
filters={filters ?? {}}
|
|
||||||
handleFiltersUpdate={handleFilters}
|
|
||||||
handleDisplayFiltersUpdate={(val) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
updateDisplayFilters(workspaceSlug.toString(), val);
|
|
||||||
}}
|
|
||||||
memberIds={workspaceMemberIds ?? undefined}
|
|
||||||
/>
|
|
||||||
</FiltersDropdown>
|
|
||||||
</div>
|
</div>
|
||||||
{isAuthorizedUser && !isArchived && (
|
{isAuthorizedUser && !isArchived ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -188,8 +122,10 @@ export const ProjectsBaseHeader = observer(() => {
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline-block">Add</span> Project
|
<span className="hidden sm:inline-block">Add</span> Project
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
</div>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { observer } from "mobx-react";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
|
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project";
|
import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -32,9 +33,8 @@ const Root = observer(() => {
|
||||||
|
|
||||||
const isArchived = pathname.includes("/archives");
|
const isArchived = pathname.includes("/archives");
|
||||||
|
|
||||||
const allowedDisplayFilters = currentWorkspaceAppliedDisplayFilters?.filter(
|
const allowedDisplayFilters =
|
||||||
(filter) => filter !== "archived_projects"
|
currentWorkspaceAppliedDisplayFilters?.filter((filter) => filter !== "archived_projects") ?? [];
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const handleRemoveFilter = useCallback(
|
const handleRemoveFilter = useCallback(
|
||||||
(key: keyof TProjectFilters, value: string | null) => {
|
(key: keyof TProjectFilters, value: string | null) => {
|
||||||
|
|
@ -65,17 +65,17 @@ const Root = observer(() => {
|
||||||
}, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]);
|
}, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isArchived ? updateDisplayFilters(workspaceSlug.toString(), { archived_projects: true }) :
|
isArchived
|
||||||
updateDisplayFilters(workspaceSlug.toString(), { archived_projects: false });
|
? updateDisplayFilters(workspaceSlug.toString(), { archived_projects: true })
|
||||||
|
: updateDisplayFilters(workspaceSlug.toString(), { archived_projects: false });
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 ||
|
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || allowedDisplayFilters.length > 0) && (
|
||||||
(allowedDisplayFilters.length>0)) && (
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
|
||||||
<ProjectAppliedFiltersList
|
<ProjectAppliedFiltersList
|
||||||
appliedFilters={currentWorkspaceFilters ?? {}}
|
appliedFilters={currentWorkspaceFilters ?? {}}
|
||||||
appliedDisplayFilters={allowedDisplayFilters}
|
appliedDisplayFilters={allowedDisplayFilters}
|
||||||
|
|
@ -86,7 +86,7 @@ const Root = observer(() => {
|
||||||
totalProjects={totalProjectIds?.length ?? 0}
|
totalProjects={totalProjectIds?.length ?? 0}
|
||||||
alwaysAllowEditing
|
alwaysAllowEditing
|
||||||
/>
|
/>
|
||||||
</div>
|
</CustomHeader>
|
||||||
)}
|
)}
|
||||||
<ProjectCardList />
|
<ProjectCardList />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const SpreadsheetIssueRowLoader = (props: { columnCount: number }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SpreadsheetLayoutLoader = () => (
|
export const SpreadsheetLayoutLoader = () => (
|
||||||
<div className="horizontal-scroll-enable h-full w-full ">
|
<div className="horizontal-scroll-enable h-full w-full overflow-y-auto ">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
|
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
|
||||||
// hooks
|
// hooks
|
||||||
import { useWorkspaceNotifications } from "@/hooks/store";
|
import { useWorkspaceNotifications } from "@/hooks/store";
|
||||||
|
|
@ -35,30 +36,32 @@ export const AppliedFilters: FC<TAppliedFilters> = observer((props) => {
|
||||||
|
|
||||||
if (!isFiltersEnabled || !workspaceSlug) return <></>;
|
if (!isFiltersEnabled || !workspaceSlug) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3 flex items-center flex-wrap gap-2">
|
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||||
{FILTER_TYPE_OPTIONS.map((filter) => {
|
<CustomHeader.LeftItem>
|
||||||
const isSelected = filters?.type?.[filter?.value] || false;
|
{FILTER_TYPE_OPTIONS.map((filter) => {
|
||||||
if (!isSelected) return <></>;
|
const isSelected = filters?.type?.[filter?.value] || false;
|
||||||
return (
|
if (!isSelected) return <></>;
|
||||||
<div
|
return (
|
||||||
key={filter.value}
|
<div
|
||||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs"
|
key={filter.value}
|
||||||
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs"
|
||||||
>
|
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
||||||
<div className="whitespace-nowrap text-custom-text-200">{filter.label}</div>
|
>
|
||||||
<div className="w-4 h-4 flex justify-center items-center transition-all rounded-sm bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100">
|
<div className="whitespace-nowrap text-custom-text-200">{filter.label}</div>
|
||||||
<X className="h-3 w-3" />
|
<div className="w-4 h-4 flex justify-center items-center transition-all rounded-sm bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||||
onClick={handleClearFilters}
|
onClick={handleClearFilters}
|
||||||
>
|
>
|
||||||
<div className="whitespace-nowrap">Clear all</div>
|
<div className="whitespace-nowrap">Clear all</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Inbox } from "lucide-react";
|
import { Inbox } from "lucide-react";
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomHeader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { SidebarHamburgerToggle } from "@/components/core";
|
import { SidebarHamburgerToggle } from "@/components/core";
|
||||||
|
|
@ -18,8 +18,8 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
||||||
|
|
||||||
if (!workspaceSlug) return <></>;
|
if (!workspaceSlug) return <></>;
|
||||||
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 bg-custom-sidebar-background-100 p-4">
|
<CustomHeader className="my-auto">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<CustomHeader.LeftItem>
|
||||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,9 +31,10 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</CustomHeader.LeftItem>
|
||||||
|
<CustomHeader.RightItem>
|
||||||
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
|
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
|
||||||
</div>
|
</CustomHeader.RightItem>
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, CustomRow, EHeaderVariant, ERowVariant } from "@plane/ui";
|
||||||
import { CountChip } from "@/components/common";
|
import { CountChip } from "@/components/common";
|
||||||
import {
|
import {
|
||||||
NotificationsLoader,
|
NotificationsLoader,
|
||||||
|
|
@ -46,15 +47,15 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
||||||
<div className="border-b border-custom-border-200">
|
<CustomRow className="h-[3.75rem] border-b border-custom-border-200 flex">
|
||||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||||
</div>
|
</CustomRow>
|
||||||
|
|
||||||
<div className="flex-shrink-0 w-full h-[46px] border-b border-custom-border-200 px-5 relative flex items-center gap-2">
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||||
{NOTIFICATION_TABS.map((tab) => (
|
{NOTIFICATION_TABS.map((tab) => (
|
||||||
<div
|
<div
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
className="h-full px-3 relative flex flex-col cursor-pointer"
|
className="h-full px-3 relative cursor-pointer"
|
||||||
onClick={() => currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)}
|
onClick={() => currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -75,12 +76,10 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</CustomHeader>
|
||||||
|
|
||||||
{/* applied filters */}
|
{/* applied filters */}
|
||||||
<div className="flex-shrink-0">
|
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* rendering notifications */}
|
{/* rendering notifications */}
|
||||||
{loader === "init-loader" ? (
|
{loader === "init-loader" ? (
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Plus } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TStaticViewTypes } from "@plane/types";
|
import { TStaticViewTypes } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
|
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||||
import {
|
import {
|
||||||
CreateUpdateWorkspaceViewModal,
|
CreateUpdateWorkspaceViewModal,
|
||||||
DefaultWorkspaceViewQuickActions,
|
DefaultWorkspaceViewQuickActions,
|
||||||
|
|
@ -103,30 +104,30 @@ export const GlobalViewsHeader: React.FC = observer(() => {
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex gap-4">
|
||||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||||
<div className="group relative flex border-b border-custom-border-200">
|
<div
|
||||||
<div
|
ref={containerRef}
|
||||||
ref={containerRef}
|
className="flex h-full w-full items-center overflow-x-auto horizontal-scrollbar scrollbar-sm"
|
||||||
className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm"
|
>
|
||||||
>
|
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
|
||||||
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
|
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />
|
||||||
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />
|
))}
|
||||||
))}
|
|
||||||
|
|
||||||
{currentWorkspaceViews?.map((viewId) => <ViewTab key={viewId} viewId={viewId} />)}
|
{currentWorkspaceViews?.map((viewId) => <ViewTab key={viewId} viewId={viewId} />)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{isAuthorizedUser && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
|
|
||||||
onClick={() => setCreateViewModal(true)}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 text-custom-primary-200" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{isAuthorizedUser ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sticky -right-4 flex flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
|
||||||
|
onClick={() => setCreateViewModal(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 text-custom-primary-200" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue