mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1176] chore: filter dropdown indicator and code refactor (#4379)
* chore: filter dropdown indicator and code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: refactor calculateTotalFilters function with typescript generics
This commit is contained in:
parent
1eba6c24cd
commit
2aef40b7c5
@ -11,6 +11,7 @@ import { CycleFiltersSelection } from "@/components/cycles";
|
|||||||
import { FiltersDropdown } from "@/components/issues";
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycleFilter } from "@/hooks/store";
|
import { useCycleFilter } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -61,6 +62,8 @@ export const ArchivedCyclesHeader: FC = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex border-b border-custom-border-200">
|
<div className="group relative flex border-b border-custom-border-200">
|
||||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||||
@ -110,7 +113,12 @@ export const ArchivedCyclesHeader: FC = observer(() => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<CycleFiltersSelection
|
<CycleFiltersSelection
|
||||||
filters={currentProjectArchivedFilters ?? {}}
|
filters={currentProjectArchivedFilters ?? {}}
|
||||||
handleFiltersUpdate={handleFilters}
|
handleFiltersUpdate={handleFilters}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
//components
|
|
||||||
// icons
|
// icons
|
||||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// hooks
|
// components
|
||||||
// constants
|
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||||
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||||
|
|
||||||
export const CycleMobileHeader = () => {
|
export const CycleMobileHeader = () => {
|
||||||
@ -103,6 +107,8 @@ export const CycleMobileHeader = () => {
|
|||||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -142,6 +148,7 @@ export const CycleMobileHeader = () => {
|
|||||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
// icons
|
||||||
import { ListFilter, Search, X } from "lucide-react";
|
import { ListFilter, Search, X } from "lucide-react";
|
||||||
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
// types
|
// types
|
||||||
import { TCycleFilters } from "@plane/types";
|
import { TCycleFilters } from "@plane/types";
|
||||||
@ -13,6 +15,7 @@ import { FiltersDropdown } from "@/components/issues";
|
|||||||
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
|
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycleFilter } from "@/hooks/store";
|
import { useCycleFilter } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -75,6 +78,8 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[50px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-6 sm:pb-0">
|
<div className="h-[50px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-6 sm:pb-0">
|
||||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||||
@ -135,7 +140,12 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
|
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||||
|
@ -2,19 +2,25 @@ import { useCallback, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// icons
|
||||||
// components
|
|
||||||
import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
import {
|
import {
|
||||||
useApplication,
|
useApplication,
|
||||||
useEventTracker,
|
useEventTracker,
|
||||||
@ -27,12 +33,7 @@ import {
|
|||||||
useIssues,
|
useIssues,
|
||||||
} from "@/hooks/store";
|
} from "@/hooks/store";
|
||||||
import useLocalStorage from "@/hooks/use-local-storage";
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
// ui
|
|
||||||
// icons
|
|
||||||
// helpers
|
|
||||||
// types
|
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// constants
|
|
||||||
|
|
||||||
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
||||||
// router
|
// router
|
||||||
@ -152,6 +153,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
: cycleDetails.total_issues
|
: cycleDetails.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -239,7 +242,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
onChange={(layout) => handleLayoutChange(layout)}
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -14,6 +14,8 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
|||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
|
import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
|
||||||
|
|
||||||
@ -94,6 +96,8 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
|
|||||||
|
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||||
@ -110,7 +114,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<>
|
<>
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
@ -2,18 +2,25 @@ import { useCallback, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// icons
|
||||||
import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
import {
|
import {
|
||||||
useApplication,
|
useApplication,
|
||||||
useEventTracker,
|
useEventTracker,
|
||||||
@ -27,13 +34,7 @@ import {
|
|||||||
} from "@/hooks/store";
|
} from "@/hooks/store";
|
||||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||||
import useLocalStorage from "@/hooks/use-local-storage";
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// icons
|
|
||||||
// helpers
|
|
||||||
// types
|
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// constants
|
|
||||||
|
|
||||||
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
|
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
|
||||||
// router
|
// router
|
||||||
@ -152,6 +153,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
: moduleDetails.total_issues
|
: moduleDetails.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -240,7 +243,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
onChange={(layout) => handleLayoutChange(layout)}
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { FC, useCallback } from "react";
|
import { FC, useCallback } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
// hooks
|
|
||||||
// components
|
|
||||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
|
||||||
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
// helper
|
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
@ -83,6 +85,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
: currentProjectDetails.draft_issues
|
: currentProjectDetails.draft_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.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">
|
<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">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@ -131,7 +135,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
onChange={(layout) => handleLayoutChange(layout)}
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
// icons
|
||||||
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
// hooks
|
// ui
|
||||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import {
|
import {
|
||||||
useApplication,
|
useApplication,
|
||||||
useEventTracker,
|
useEventTracker,
|
||||||
@ -21,12 +28,7 @@ import {
|
|||||||
useMember,
|
useMember,
|
||||||
} from "@/hooks/store";
|
} from "@/hooks/store";
|
||||||
import { useIssues } from "@/hooks/store/use-issues";
|
import { useIssues } from "@/hooks/store/use-issues";
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// types
|
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// constants
|
|
||||||
// helper
|
|
||||||
|
|
||||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -109,6 +111,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
: currentProjectDetails?.total_issues
|
: currentProjectDetails?.total_issues
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -180,7 +184,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
onChange={(layout) => handleLayoutChange(layout)}
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -2,21 +2,23 @@ import { useCallback } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
// icons
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
// hooks
|
|
||||||
// components
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// helpers
|
|
||||||
// types
|
|
||||||
// constants
|
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
import {
|
import {
|
||||||
useApplication,
|
useApplication,
|
||||||
useEventTracker,
|
useEventTracker,
|
||||||
@ -128,6 +130,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
const canUserCreateIssue =
|
const canUserCreateIssue =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
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">
|
<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">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -200,7 +204,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
|
<FiltersDropdown
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
disabled={!canUserCreateIssue}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// icons
|
||||||
import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
|
import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
|
||||||
|
// types
|
||||||
import { TProjectFilters } from "@plane/types";
|
import { TProjectFilters } from "@plane/types";
|
||||||
// hooks
|
|
||||||
// components
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
// helpers
|
|
||||||
// constants
|
|
||||||
import { FiltersDropdown } from "@/components/issues";
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import { useApplication, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
import { useApplication, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
|
|
||||||
@ -78,6 +81,8 @@ export const ProjectsHeader = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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">
|
<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">
|
||||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@ -145,7 +150,12 @@ export const ProjectsHeader = observer(() => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<ProjectFiltersSelection
|
<ProjectFiltersSelection
|
||||||
displayFilters={displayFilters ?? {}}
|
displayFilters={displayFilters ?? {}}
|
||||||
filters={filters ?? {}}
|
filters={filters ?? {}}
|
||||||
|
@ -8,6 +8,8 @@ import { ArchiveTabsList } from "@/components/archives";
|
|||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||||
|
|
||||||
@ -62,6 +64,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex border-b border-custom-border-200">
|
<div className="group relative flex border-b border-custom-border-200">
|
||||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||||
@ -69,7 +73,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/* filter options */}
|
{/* filter options */}
|
||||||
<div className="flex items-center gap-2 px-8">
|
<div className="flex items-center gap-2 px-8">
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters || {}}
|
filters={issueFilters?.filters || {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
|
// icons
|
||||||
import { ChevronUp } from "lucide-react";
|
import { ChevronUp } from "lucide-react";
|
||||||
|
// headless ui
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -15,10 +16,20 @@ type Props = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
menuButton?: React.ReactNode;
|
menuButton?: React.ReactNode;
|
||||||
|
isFiltersApplied?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FiltersDropdown: React.FC<Props> = (props) => {
|
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||||
const { children, icon, title = "Dropdown", placement, disabled = false, tabIndex, menuButton } = props;
|
const {
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
title = "Dropdown",
|
||||||
|
placement,
|
||||||
|
disabled = false,
|
||||||
|
tabIndex,
|
||||||
|
menuButton,
|
||||||
|
isFiltersApplied = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -50,10 +61,16 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
|||||||
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
||||||
}
|
}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
className="relative"
|
||||||
>
|
>
|
||||||
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
<>
|
||||||
<span>{title}</span>
|
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||||
</div>
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
{isFiltersApplied && (
|
||||||
|
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
@ -73,7 +90,9 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
|||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
>
|
>
|
||||||
<div className="flex max-h-[30rem] lg:max-h-[37.5rem] w-[18.75rem] flex-col overflow-hidden">{children}</div>
|
<div className="flex max-h-[30rem] lg:max-h-[37.5rem] w-[18.75rem] flex-col overflow-hidden">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
// components
|
|
||||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
|
||||||
import { CustomMenu } from "@plane/ui";
|
|
||||||
// icons
|
// icons
|
||||||
// constants
|
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||||
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||||
// layouts
|
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
|
|
||||||
|
|
||||||
export const IssuesMobileHeader = observer(() => {
|
export const IssuesMobileHeader = observer(() => {
|
||||||
const layouts = [
|
const layouts = [
|
||||||
@ -83,6 +86,8 @@ export const IssuesMobileHeader = observer(() => {
|
|||||||
[workspaceSlug, projectId, updateFilters]
|
[workspaceSlug, projectId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -122,6 +127,7 @@ export const IssuesMobileHeader = observer(() => {
|
|||||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
@ -11,6 +11,7 @@ import { FiltersDropdown } from "@/components/issues";
|
|||||||
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules";
|
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useModuleFilter } from "@/hooks/store";
|
import { useMember, useModuleFilter } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -70,6 +71,8 @@ export const ArchivedModulesHeader: FC = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex border-b border-custom-border-200">
|
<div className="group relative flex border-b border-custom-border-200">
|
||||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||||
@ -128,7 +131,12 @@ export const ArchivedModulesHeader: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<ModuleFiltersSelection
|
<ModuleFiltersSelection
|
||||||
displayFilters={currentProjectDisplayFilters ?? {}}
|
displayFilters={currentProjectDisplayFilters ?? {}}
|
||||||
filters={currentProjectArchivedFilters ?? {}}
|
filters={currentProjectArchivedFilters ?? {}}
|
||||||
|
@ -3,17 +3,19 @@ import { observer } from "mobx-react";
|
|||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
// icons
|
// icons
|
||||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||||
// hooks
|
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
|
||||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
|
||||||
// types
|
|
||||||
// constants
|
// constants
|
||||||
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
|
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||||
|
|
||||||
export const ModuleMobileHeader = observer(() => {
|
export const ModuleMobileHeader = observer(() => {
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
@ -86,6 +88,8 @@ export const ModuleMobileHeader = observer(() => {
|
|||||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block md:hidden">
|
<div className="block md:hidden">
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -125,6 +129,7 @@ export const ModuleMobileHeader = observer(() => {
|
|||||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { FC, useCallback, useRef, useState } from "react";
|
import React, { FC, useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
// icons
|
||||||
import { ListFilter, Search, X } from "lucide-react";
|
import { ListFilter, Search, X } from "lucide-react";
|
||||||
|
// helpers
|
||||||
import { cn } from "@plane/editor-core";
|
import { cn } from "@plane/editor-core";
|
||||||
// types
|
// types
|
||||||
import { TModuleFilters } from "@plane/types";
|
import { TModuleFilters } from "@plane/types";
|
||||||
@ -12,6 +14,8 @@ import { FiltersDropdown } from "@/components/issues";
|
|||||||
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules/dropdowns";
|
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules/dropdowns";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_VIEW_LAYOUTS } from "@/constants/module";
|
import { MODULE_VIEW_LAYOUTS } from "@/constants/module";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useModuleFilter } from "@/hooks/store";
|
import { useMember, useModuleFilter } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -79,6 +83,9 @@ export const ModuleViewHeader: FC = observer(() => {
|
|||||||
useOutsideClickDetector(inputRef, () => {
|
useOutsideClickDetector(inputRef, () => {
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -135,7 +142,12 @@ export const ModuleViewHeader: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<ModuleFiltersSelection
|
<ModuleFiltersSelection
|
||||||
displayFilters={displayFilters ?? {}}
|
displayFilters={displayFilters ?? {}}
|
||||||
filters={filters ?? {}}
|
filters={filters ?? {}}
|
||||||
|
@ -45,6 +45,8 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
|||||||
[filters.filters, updateFilters]
|
[filters.filters, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
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">
|
<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">
|
||||||
@ -59,7 +61,12 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
|||||||
if (val.order) updateFilters("sortBy", val.order);
|
if (val.order) updateFilters("sortBy", val.order);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
<FiltersDropdown
|
||||||
|
icon={<ListFilter className="h-3 w-3" />}
|
||||||
|
title="Filters"
|
||||||
|
placement="bottom-end"
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
|
>
|
||||||
<PageFiltersSelection
|
<PageFiltersSelection
|
||||||
filters={filters}
|
filters={filters}
|
||||||
handleFiltersUpdate={updateFilters}
|
handleFiltersUpdate={updateFilters}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues";
|
||||||
// hooks
|
|
||||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
|
||||||
import { useIssues, useLabel } from "@/hooks/store";
|
|
||||||
// constants
|
// constants
|
||||||
|
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
|
import { useIssues, useLabel } from "@/hooks/store";
|
||||||
|
|
||||||
export const ProfileIssuesFilter = observer(() => {
|
export const ProfileIssuesFilter = observer(() => {
|
||||||
// router
|
// router
|
||||||
@ -93,6 +96,8 @@ export const ProfileIssuesFilter = observer(() => {
|
|||||||
[workspaceSlug, updateFilters, userId]
|
[workspaceSlug, updateFilters, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-end gap-2">
|
<div className="relative flex items-center justify-end gap-2">
|
||||||
<LayoutSelection
|
<LayoutSelection
|
||||||
@ -101,7 +106,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||||||
selectedLayout={activeLayout}
|
selectedLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
|
||||||
|
@ -11,10 +11,11 @@ import { CustomMenu } from "@plane/ui";
|
|||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useLabel } from "@/hooks/store";
|
import { useIssues, useLabel } from "@/hooks/store";
|
||||||
|
|
||||||
|
|
||||||
const ProfileIssuesMobileHeader = observer(() => {
|
const ProfileIssuesMobileHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -99,6 +100,9 @@ const ProfileIssuesMobileHeader = observer(() => {
|
|||||||
},
|
},
|
||||||
[workspaceSlug, updateFilters, userId]
|
[workspaceSlug, updateFilters, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
|
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
@ -135,6 +139,7 @@ const ProfileIssuesMobileHeader = observer(() => {
|
|||||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
|
@ -5,10 +5,12 @@ import { ChevronDown, ListFilter } from "lucide-react";
|
|||||||
// types
|
// types
|
||||||
import { TProjectFilters } from "@plane/types";
|
import { TProjectFilters } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||||
|
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project/dropdowns";
|
||||||
|
// helpers
|
||||||
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import { useApplication, useMember, useProjectFilter } from "@/hooks/store";
|
import { useApplication, useMember, useProjectFilter } from "@/hooks/store";
|
||||||
// components
|
|
||||||
import { FiltersDropdown } from "../issues";
|
|
||||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "./dropdowns";
|
|
||||||
|
|
||||||
const ProjectsMobileHeader = observer(() => {
|
const ProjectsMobileHeader = observer(() => {
|
||||||
const {
|
const {
|
||||||
@ -44,6 +46,8 @@ const ProjectsMobileHeader = observer(() => {
|
|||||||
[filters, updateFilters, workspaceSlug]
|
[filters, updateFilters, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100 w-full">
|
<div className="flex py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100 w-full">
|
||||||
<ProjectOrderByDropdown
|
<ProjectOrderByDropdown
|
||||||
@ -68,6 +72,7 @@ const ProjectsMobileHeader = observer(() => {
|
|||||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<ProjectFiltersSelection
|
<ProjectFiltersSelection
|
||||||
displayFilters={displayFilters ?? {}}
|
displayFilters={displayFilters ?? {}}
|
||||||
|
@ -34,7 +34,7 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
// @ts-expect-error key types are not compatible
|
|
||||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||||
// helpers
|
// helpers
|
||||||
import { getDate } from "./date-time.helper";
|
import { getDate } from "./date-time.helper";
|
||||||
// types
|
|
||||||
// import { IIssueFilterOptions } from "@plane/types";
|
// import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
|
||||||
type TFilters = {
|
|
||||||
[key: string]: boolean | string[] | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description calculates the total number of filters applied
|
* @description calculates the total number of filters applied
|
||||||
* @param {TFilters} filters
|
* @param {T} filters
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export const calculateTotalFilters = (filters: TFilters): number =>
|
export const calculateTotalFilters = <T>(filters: T): number =>
|
||||||
filters && Object.keys(filters).length > 0
|
filters && Object.keys(filters).length > 0
|
||||||
? Object.keys(filters)
|
? Object.keys(filters)
|
||||||
.map((key) => {
|
.map((key) => {
|
||||||
const value = filters[key as keyof TFilters];
|
const value = filters[key as keyof T];
|
||||||
if (value === null) return 0;
|
if (value === null) return 0;
|
||||||
if (Array.isArray(value)) return value.length;
|
if (Array.isArray(value)) return value.length;
|
||||||
if (typeof value === "boolean") return value ? 1 : 0;
|
if (typeof value === "boolean") return value ? 1 : 0;
|
||||||
@ -25,7 +20,6 @@ export const calculateTotalFilters = (filters: TFilters): number =>
|
|||||||
})
|
})
|
||||||
.reduce((curr, prev) => curr + prev, 0)
|
.reduce((curr, prev) => curr + prev, 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description checks if the date satisfies the filter
|
* @description checks if the date satisfies the filter
|
||||||
* @param {Date} date
|
* @param {Date} date
|
||||||
|
Loading…
Reference in New Issue
Block a user