mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-4969] feat: add toggle button for work item filters row visibility (#7865)
* [WEB-4969] feat: add toggle button for work item filters row visibility * fix: improve error handling in filter update and refine visibility condition check * chore: correct visibility toggle parameter in filter store
This commit is contained in:
parent
992457efd2
commit
7ce21a6488
@ -38,6 +38,7 @@ import {
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
@ -209,6 +210,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<WorkItemFiltersToggle entityType={EIssuesStoreType.CYCLE} entityId={cycleId} />
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
|
||||
@ -35,8 +35,8 @@ import {
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
@ -57,7 +57,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
const { workspaceSlug, projectId, moduleId: routerModuleId } = useParams();
|
||||
const moduleId = routerModuleId ? routerModuleId.toString() : undefined;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
// store hooks
|
||||
@ -75,7 +76,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
// derived values
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId) : undefined;
|
||||
const canUserCreateIssue = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
@ -197,6 +198,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
{moduleId && <WorkItemFiltersToggle entityType={EIssuesStoreType.MODULE} entityId={moduleId} />}
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
@ -251,13 +253,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
>
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
<ModuleQuickActions
|
||||
parentRef={parentRef}
|
||||
moduleId={moduleId?.toString()}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center bg-custom-background-80/70 rounded size-[26px]"
|
||||
/>
|
||||
{moduleId && (
|
||||
<ModuleQuickActions
|
||||
parentRef={parentRef}
|
||||
moduleId={moduleId}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center bg-custom-background-80/70 rounded size-[26px]"
|
||||
/>
|
||||
)}
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
|
||||
@ -31,6 +31,7 @@ import { SwitcherIcon, SwitcherLabel } from "@/components/common/switcher-label"
|
||||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views/quick-actions";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
@ -45,8 +46,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// router
|
||||
const { workspaceSlug, projectId, viewId } = useParams();
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, viewId: routerViewId } = useParams();
|
||||
const viewId = routerViewId ? routerViewId.toString() : undefined;
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
@ -163,8 +165,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
)}
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
{!viewDetails?.is_locked ? (
|
||||
<>
|
||||
<>
|
||||
{!viewDetails.is_locked && (
|
||||
<LayoutSelection
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
@ -176,6 +178,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
)}
|
||||
{viewId && <WorkItemFiltersToggle entityType={EIssuesStoreType.PROJECT_VIEW} entityId={viewId} />}
|
||||
{!viewDetails.is_locked && (
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
@ -189,10 +194,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
{canUserCreateIssue ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@ -25,6 +25,7 @@ import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
import { DefaultWorkspaceViewQuickActions } from "@/components/workspace/views/default-view-quick-action";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal";
|
||||
import { WorkspaceViewQuickActions } from "@/components/workspace/views/quick-action";
|
||||
@ -39,7 +40,8 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
const [createViewModal, setCreateViewModal] = useState(false);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, globalViewId } = useParams();
|
||||
const { workspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const globalViewId = routerGlobalViewId ? routerGlobalViewId.toString() : undefined;
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { filters, updateFilters },
|
||||
@ -50,7 +52,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const viewDetails = getViewDetailsById(globalViewId.toString());
|
||||
const viewDetails = globalViewId ? getViewDetailsById(globalViewId) : undefined;
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
@ -60,7 +62,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
updatedDisplayFilter,
|
||||
globalViewId.toString()
|
||||
globalViewId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
@ -69,13 +71,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
property,
|
||||
globalViewId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_PROPERTIES, property, globalViewId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
);
|
||||
@ -88,7 +84,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout },
|
||||
globalViewId.toString()
|
||||
globalViewId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
@ -158,25 +154,24 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
</Header.LeftItem>
|
||||
|
||||
<Header.RightItem className="items-center">
|
||||
{!isLocked ? (
|
||||
<>
|
||||
<GlobalViewLayoutSelection
|
||||
onChange={handleLayoutChange}
|
||||
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
{!isLocked && (
|
||||
<GlobalViewLayoutSelection
|
||||
onChange={handleLayoutChange}
|
||||
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
{globalViewId && <WorkItemFiltersToggle entityType={EIssuesStoreType.GLOBAL} entityId={globalViewId} />}
|
||||
{!isLocked && (
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
|
||||
<Button
|
||||
|
||||
@ -9,13 +9,16 @@ import { EHeaderVariant, Header } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveTabsList } from "@/components/archives";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
@ -29,7 +32,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, {
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, {
|
||||
...issueFilters?.displayFilters,
|
||||
...updatedDisplayFilter,
|
||||
});
|
||||
@ -38,15 +41,17 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
const handleDisplayPropertiesUpdate = (property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
return (
|
||||
<Header variant={EHeaderVariant.SECONDARY}>
|
||||
<Header.LeftItem>
|
||||
<ArchiveTabsList />
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<WorkItemFiltersToggle entityType={EIssuesStoreType.ARCHIVED} entityId={projectId} />
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilters?.displayFilters || {}}
|
||||
|
||||
@ -14,6 +14,7 @@ import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { TProject } from "@/plane-web/types";
|
||||
// local imports
|
||||
import { WorkItemsModal } from "../analytics/work-items/modal";
|
||||
import { WorkItemFiltersToggle } from "../work-item-filters/filters-toggle";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
@ -102,6 +103,7 @@ export const HeaderFilters = observer((props: Props) => {
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
||||
<FiltersDropdown
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
title={t("common.display")}
|
||||
|
||||
@ -10,7 +10,7 @@ import { EmptyState } from "@/components/common/empty-state";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceActiveLayout } from "@/components/views/helper";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
@ -9,7 +9,7 @@ import { EIssuesStoreType } from "@plane/types";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
// hooks
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
|
||||
@ -12,7 +12,7 @@ import { TransferIssues } from "@/components/cycles/transfer-issues";
|
||||
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
@ -10,7 +10,7 @@ import { Row, ERowVariant } from "@plane/ui";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
|
||||
@ -11,7 +11,7 @@ import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
@ -9,7 +9,7 @@ import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
@ -9,6 +9,7 @@ import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
@ -16,7 +17,8 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const { workspaceSlug, userId: routeUserId } = useParams();
|
||||
const userId = routeUserId ? routeUserId.toString() : undefined;
|
||||
// store hook
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
@ -27,13 +29,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout },
|
||||
userId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, userId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
@ -46,7 +42,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
updatedDisplayFilter,
|
||||
userId.toString()
|
||||
userId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
@ -55,13 +51,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
property,
|
||||
userId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_PROPERTIES, property, userId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
@ -73,6 +63,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
{userId && <WorkItemFiltersToggle entityType={EIssuesStoreType.PROFILE} entityId={userId} />}
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
||||
@ -10,7 +10,7 @@ import { ProfileIssuesKanBanLayout } from "@/components/issues/issue-layouts/kan
|
||||
import { ProfileIssuesListLayout } from "@/components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
@ -10,7 +10,7 @@ import { cn, getOperatorForPayload } from "@plane/utils";
|
||||
|
||||
export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
buttonConfig?: {
|
||||
label?: string;
|
||||
label: string | null;
|
||||
variant?: TButtonVariant;
|
||||
className?: string;
|
||||
defaultOpen?: boolean;
|
||||
@ -28,7 +28,7 @@ export const AddFilterButton = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterButtonProps<P, E>) => {
|
||||
const { filter, buttonConfig, onFilterSelect } = props;
|
||||
const {
|
||||
label = "Filters",
|
||||
label,
|
||||
variant = "link-neutral",
|
||||
className,
|
||||
defaultOpen = false,
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilterPlus } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { EHeaderVariant, Header } from "@plane/ui";
|
||||
import { cn, EHeaderVariant, Header } from "@plane/ui";
|
||||
// local imports
|
||||
import { AddFilterButton, TAddFilterButtonProps } from "./add-filters-button";
|
||||
import { FilterItem } from "./filter-item";
|
||||
@ -14,8 +15,7 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
||||
buttonConfig?: TAddFilterButtonProps<K, E>["buttonConfig"];
|
||||
disabledAllOperations?: boolean;
|
||||
filter: IFilterInstance<K, E>;
|
||||
variant?: "default" | "header";
|
||||
visible?: boolean;
|
||||
variant?: "modal" | "header";
|
||||
trackerElements?: {
|
||||
clearFilter?: string;
|
||||
saveView?: string;
|
||||
@ -25,25 +25,35 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
||||
|
||||
export const FiltersRow = observer(
|
||||
<K extends TFilterProperty, E extends TExternalFilter>(props: TFiltersRowProps<K, E>) => {
|
||||
const {
|
||||
buttonConfig,
|
||||
disabledAllOperations = false,
|
||||
filter,
|
||||
variant = "header",
|
||||
visible = true,
|
||||
trackerElements,
|
||||
} = props;
|
||||
const { buttonConfig, disabledAllOperations = false, filter, variant = "header", trackerElements } = props;
|
||||
// states
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
// derived values
|
||||
const hasAnyConditions = filter.allConditionsForDisplay.length > 0;
|
||||
const hasAvailableOperations =
|
||||
!disabledAllOperations && (filter.canClearFilters || filter.canSaveView || filter.canUpdateView);
|
||||
|
||||
const headerButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
||||
variant: "link-neutral",
|
||||
className: "bg-custom-background-90",
|
||||
label: null,
|
||||
};
|
||||
|
||||
const modalButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
||||
variant: "neutral-primary",
|
||||
className: "bg-custom-background-100",
|
||||
label: !hasAnyConditions ? "Filters" : null,
|
||||
};
|
||||
|
||||
const handleUpdate = useCallback(async () => {
|
||||
setIsUpdating(true);
|
||||
await filter.updateView();
|
||||
setTimeout(() => setIsUpdating(false), 240); // To avoid flickering
|
||||
try {
|
||||
await filter.updateView();
|
||||
} finally {
|
||||
setTimeout(() => setIsUpdating(false), 240); // To avoid flickering
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const leftContent = (
|
||||
<>
|
||||
{filter.allConditionsForDisplay.map((condition) => (
|
||||
@ -52,7 +62,13 @@ export const FiltersRow = observer(
|
||||
<AddFilterButton
|
||||
filter={filter}
|
||||
buttonConfig={{
|
||||
variant: "neutral-primary",
|
||||
label: null,
|
||||
...(variant === "modal" ? modalButtonConfig : headerButtonConfig),
|
||||
iconConfig: {
|
||||
shouldShowIcon: true,
|
||||
iconComponent: ListFilterPlus,
|
||||
},
|
||||
defaultOpen: buttonConfig?.defaultOpen ?? !hasAnyConditions,
|
||||
...buttonConfig,
|
||||
isDisabled: disabledAllOperations,
|
||||
}}
|
||||
@ -100,22 +116,43 @@ export const FiltersRow = observer(
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === "default") {
|
||||
return (
|
||||
<div className="w-full flex flex-wrap items-center gap-2">
|
||||
{leftContent}
|
||||
const mainContent = (
|
||||
<div className="w-full flex items-start gap-2">
|
||||
<div className="w-full flex flex-wrap items-center gap-2">{leftContent}</div>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 border-l border-custom-border-200 pl-4", {
|
||||
"border-l-transparent pl-0": !hasAvailableOperations,
|
||||
})}
|
||||
>
|
||||
{rightContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ModalVariant = (
|
||||
<div className="w-full flex flex-wrap items-center gap-2 min-h-11 bg-custom-background-90 rounded-lg p-2">
|
||||
{mainContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
const HeaderVariant = (
|
||||
<Header variant={EHeaderVariant.TERNARY} className="min-h-11">
|
||||
{mainContent}
|
||||
</Header>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<div className="w-full flex items-start gap-2">
|
||||
<div className="w-full flex flex-wrap items-center gap-2">{leftContent}</div>
|
||||
<div className="flex items-center gap-2">{rightContent}</div>
|
||||
</div>
|
||||
</Header>
|
||||
<Transition
|
||||
show={filter.isVisible}
|
||||
enter="transition-all duration-150 ease-out"
|
||||
enterFrom="opacity-0 -translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition-all duration-100 ease-in"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
{variant === "modal" ? ModalVariant : HeaderVariant}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
76
apps/web/core/components/rich-filters/filters-toggle.tsx
Normal file
76
apps/web/core/components/rich-filters/filters-toggle.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { cn } from "@plane/ui";
|
||||
// components
|
||||
import { AddFilterButton } from "@/components/rich-filters/add-filters-button";
|
||||
|
||||
type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
filter: IFilterInstance<P, E> | undefined;
|
||||
};
|
||||
|
||||
const COMMON_CLASSNAME =
|
||||
"grid place-items-center h-7 w-full py-0.5 px-2 rounded border transition-all duration-200 cursor-pointer";
|
||||
|
||||
export const FiltersToggle = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TFiltersToggleProps<P, E>) => {
|
||||
const { filter } = props;
|
||||
// derived values
|
||||
const hasAnyConditions = (filter?.allConditionsForDisplay.length ?? 0) > 0;
|
||||
const isFilterRowVisible = filter?.isVisible ?? false;
|
||||
const hasUpdates = filter?.canUpdateView === true && filter?.hasChanges === true;
|
||||
const showFilterRowChangesPill = hasUpdates || hasAnyConditions === true;
|
||||
const showAddFilterButton = !hasAnyConditions && !isFilterRowVisible && !hasUpdates;
|
||||
|
||||
const handleToggleFilter = () => {
|
||||
if (!filter) {
|
||||
console.error("Filters toggle error - filter instance not available");
|
||||
return;
|
||||
}
|
||||
filter.toggleVisibility();
|
||||
};
|
||||
|
||||
// Show the add filter button when there are no active conditions, the filter row is hidden, and no unsaved changes exist
|
||||
if (filter && showAddFilterButton) {
|
||||
return (
|
||||
<AddFilterButton
|
||||
filter={filter}
|
||||
buttonConfig={{
|
||||
variant: "neutral-primary",
|
||||
className: COMMON_CLASSNAME,
|
||||
label: null,
|
||||
}}
|
||||
onFilterSelect={() => filter?.toggleVisibility(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(COMMON_CLASSNAME, {
|
||||
"border-transparent bg-custom-primary-100/10 hover:bg-custom-primary-100/20": isFilterRowVisible,
|
||||
"border-custom-border-200 hover:bg-custom-background-90": !isFilterRowVisible,
|
||||
})}
|
||||
onClick={handleToggleFilter}
|
||||
>
|
||||
<div className="relative">
|
||||
<ListFilter
|
||||
className={cn("size-4", {
|
||||
"text-custom-primary-100": isFilterRowVisible,
|
||||
"text-custom-text-300": !isFilterRowVisible,
|
||||
})}
|
||||
/>
|
||||
{showFilterRowChangesPill && (
|
||||
<span
|
||||
className={cn("p-[3px] rounded-full bg-custom-primary-100 absolute top-[0.2px] -right-[0.4px]", {
|
||||
"bg-custom-text-300": hasAnyConditions === false && filter?.hasChanges === true, // If there are no conditions and there are changes, show the pill in the background color
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -23,7 +23,7 @@ import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex }
|
||||
// components
|
||||
import { Logo } from "@/components/common/logo";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@ -265,11 +265,12 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
projectId={projectId}
|
||||
showOnMount
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectViewWorkItemsFilter }) =>
|
||||
projectViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="default" />
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="modal" />
|
||||
)
|
||||
}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
|
||||
@ -3,8 +3,8 @@ import { observer } from "mobx-react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { IIssueFilters, TWorkItemFilterExpression } from "@plane/types";
|
||||
// store hooks
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
// plane web imports
|
||||
@ -39,9 +39,7 @@ export const WorkItemFiltersHOC = observer((props: TWorkItemFiltersHOCProps) =>
|
||||
type TWorkItemFilterProps = TSharedWorkItemFiltersProps &
|
||||
TAdditionalWorkItemFiltersProps & {
|
||||
initialWorkItemFilters: IIssueFilters;
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: { filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> }) => React.ReactNode);
|
||||
children: React.ReactNode | ((props: { filter: IWorkItemFilterInstance }) => React.ReactNode);
|
||||
};
|
||||
|
||||
const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
||||
@ -55,6 +53,7 @@ const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
||||
saveViewOptions,
|
||||
updateFilters,
|
||||
updateViewOptions,
|
||||
showOnMount,
|
||||
...entityConfigProps
|
||||
} = props;
|
||||
// store hooks
|
||||
@ -84,6 +83,7 @@ const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
||||
saveViewOptions,
|
||||
updateViewOptions,
|
||||
},
|
||||
showOnMount,
|
||||
});
|
||||
|
||||
// delete filter instance when component unmounts
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
|
||||
export type TSharedWorkItemFiltersProps = {
|
||||
@ -8,14 +8,11 @@ export type TSharedWorkItemFiltersProps = {
|
||||
filtersToShowByLayout: TWorkItemFilterProperty[];
|
||||
updateFilters: (updatedFilters: TWorkItemFilterExpression) => void;
|
||||
isTemporary?: boolean;
|
||||
showOnMount?: boolean;
|
||||
} & ({ isTemporary: true; entityId?: string } | { isTemporary?: false; entityId: string }); // entity id (project_id, cycle_id, workspace_id, etc)
|
||||
|
||||
export type TSharedWorkItemFiltersHOCProps = TSharedWorkItemFiltersProps & {
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined;
|
||||
}) => React.ReactNode);
|
||||
children: React.ReactNode | ((props: { filter: IWorkItemFilterInstance | undefined }) => React.ReactNode);
|
||||
initialWorkItemFilters: IIssueFilters | undefined;
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// components
|
||||
import { FiltersRow, TFiltersRowProps } from "@/components/rich-filters/filters-row";
|
||||
|
||||
type TWorkItemFiltersRowProps = TFiltersRowProps<TWorkItemFilterProperty, TWorkItemFilterExpression>;
|
||||
type TWorkItemFiltersRowProps = TFiltersRowProps<TWorkItemFilterProperty, TWorkItemFilterExpression> & {
|
||||
filter: IWorkItemFilterInstance;
|
||||
};
|
||||
|
||||
export const WorkItemFiltersRow = observer((props: TWorkItemFiltersRowProps) => <FiltersRow {...props} />);
|
||||
@ -0,0 +1,22 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// components
|
||||
import { FiltersToggle } from "@/components/rich-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
|
||||
type TWorkItemFiltersToggleProps = {
|
||||
entityType: EIssuesStoreType;
|
||||
entityId: string;
|
||||
};
|
||||
|
||||
export const WorkItemFiltersToggle = observer((props: TWorkItemFiltersToggleProps) => {
|
||||
const { entityType, entityId } = props;
|
||||
// store hooks
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
// derived values
|
||||
const filter = getFilter(entityType, entityId);
|
||||
|
||||
return <FiltersToggle filter={filter} />;
|
||||
});
|
||||
@ -21,7 +21,7 @@ import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
// plane web imports
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { AccessController } from "@/plane-web/components/views/access-controller";
|
||||
|
||||
type Props = {
|
||||
@ -176,11 +176,12 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
showOnMount
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: workspaceViewWorkItemsFilter }) =>
|
||||
workspaceViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={workspaceViewWorkItemsFilter} variant="default" />
|
||||
<WorkItemFiltersRow filter={workspaceViewWorkItemsFilter} variant="modal" />
|
||||
)
|
||||
}
|
||||
</WorkspaceLevelWorkItemFiltersHOC>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// local imports
|
||||
import { useWorkItemFilters } from "./use-work-item-filters";
|
||||
|
||||
export const useWorkItemFilterInstance = (
|
||||
entityType: EIssuesStoreType,
|
||||
entityId: string
|
||||
): IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined => {
|
||||
): IWorkItemFilterInstance | undefined => {
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
return getFilter(entityType, entityId);
|
||||
};
|
||||
|
||||
@ -52,6 +52,25 @@ export type TExpressionOptions<E extends TExternalFilter> = {
|
||||
*/
|
||||
export const DEFAULT_FILTER_EXPRESSION_OPTIONS: TExpressionOptions<TExternalFilter> = {};
|
||||
|
||||
/**
|
||||
* Auto visibility options.
|
||||
*/
|
||||
export type TAutoVisibilityOptions =
|
||||
| {
|
||||
autoSetVisibility: true;
|
||||
}
|
||||
| {
|
||||
autoSetVisibility: false;
|
||||
isVisibleOnMount: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default filter visibility options.
|
||||
*/
|
||||
export const DEFAULT_FILTER_VISIBILITY_OPTIONS: TAutoVisibilityOptions = {
|
||||
autoSetVisibility: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter options.
|
||||
* - expression: Filter expression options.
|
||||
@ -60,4 +79,5 @@ export const DEFAULT_FILTER_EXPRESSION_OPTIONS: TExpressionOptions<TExternalFilt
|
||||
export type TFilterOptions<E extends TExternalFilter> = {
|
||||
expression: Partial<TExpressionOptions<E>>;
|
||||
config: Partial<TConfigOptions>;
|
||||
visibility: TAutoVisibilityOptions;
|
||||
};
|
||||
|
||||
@ -58,7 +58,7 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
|
||||
filterConfigs: IFilterConfigManager<P>["filterConfigs"];
|
||||
configOptions: IFilterConfigManager<P>["configOptions"];
|
||||
// parent filter instance
|
||||
_filterInstance: IFilterInstance<P, E>;
|
||||
private _filterInstance: IFilterInstance<P, E>;
|
||||
|
||||
/**
|
||||
* Creates a new FilterConfigManager instance.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { toJS } from "mobx";
|
||||
import { action, makeObservable, observable, toJS } from "mobx";
|
||||
// plane imports
|
||||
import { DEFAULT_FILTER_EXPRESSION_OPTIONS, TExpressionOptions } from "@plane/constants";
|
||||
import { DEFAULT_FILTER_EXPRESSION_OPTIONS, TAutoVisibilityOptions, TExpressionOptions } from "@plane/constants";
|
||||
import {
|
||||
IFilterAdapter,
|
||||
LOGICAL_OPERATOR,
|
||||
@ -14,6 +14,12 @@ import {
|
||||
TFilterConditionPayload,
|
||||
} from "@plane/types";
|
||||
import { addAndCondition, createConditionNode, updateNodeInExpression } from "@plane/utils";
|
||||
// local imports
|
||||
import { type IFilterInstance } from "./filter";
|
||||
|
||||
type TFilterInstanceHelperParams<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
adapter: IFilterAdapter<P, E>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for filter instance helper utilities.
|
||||
@ -23,9 +29,13 @@ import { addAndCondition, createConditionNode, updateNodeInExpression } from "@p
|
||||
* @template E - The external filter type extending TExternalFilter
|
||||
*/
|
||||
export interface IFilterInstanceHelper<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
isVisible: boolean;
|
||||
// initialization
|
||||
initializeExpression: (initialExpression?: E) => TFilterExpression<P> | null;
|
||||
initializeExpressionOptions: (expressionOptions?: Partial<TExpressionOptions<E>>) => TExpressionOptions<E>;
|
||||
// visibility
|
||||
setInitialVisibility: (visibilityOption: TAutoVisibilityOptions) => void;
|
||||
toggleVisibility: (isVisible?: boolean) => void;
|
||||
// condition operations
|
||||
addConditionToExpression: <V extends TFilterValue>(
|
||||
expression: TFilterExpression<P> | null,
|
||||
@ -54,15 +64,28 @@ export interface IFilterInstanceHelper<P extends TFilterProperty, E extends TExt
|
||||
export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternalFilter>
|
||||
implements IFilterInstanceHelper<P, E>
|
||||
{
|
||||
// parent filter instance
|
||||
private _filterInstance: IFilterInstance<P, E>;
|
||||
// adapter
|
||||
private adapter: IFilterAdapter<P, E>;
|
||||
// visibility
|
||||
isVisible: boolean;
|
||||
|
||||
/**
|
||||
* Creates a new FilterInstanceHelper instance.
|
||||
*
|
||||
* @param adapter - The filter adapter for converting between internal and external formats
|
||||
*/
|
||||
constructor(adapter: IFilterAdapter<P, E>) {
|
||||
this.adapter = adapter;
|
||||
constructor(filterInstance: IFilterInstance<P, E>, params: TFilterInstanceHelperParams<P, E>) {
|
||||
this._filterInstance = filterInstance;
|
||||
this.adapter = params.adapter;
|
||||
this.isVisible = false;
|
||||
|
||||
makeObservable(this, {
|
||||
isVisible: observable,
|
||||
setInitialVisibility: action,
|
||||
toggleVisibility: action,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------ initialization ------------
|
||||
@ -87,6 +110,41 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
|
||||
...expressionOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the initial visibility state for the filter based on options and active filters.
|
||||
* @param visibilityOption - The visibility options for the filter instance.
|
||||
* @returns The initial visibility state
|
||||
*/
|
||||
setInitialVisibility: IFilterInstanceHelper<P, E>["setInitialVisibility"] = action((visibilityOption) => {
|
||||
// If explicit initial visibility is provided, use it
|
||||
if (visibilityOption.autoSetVisibility === false) {
|
||||
this.isVisible = visibilityOption.isVisibleOnMount;
|
||||
return;
|
||||
}
|
||||
|
||||
// If filter has active filters, make it visible
|
||||
if (this._filterInstance.hasActiveFilters) {
|
||||
this.isVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to hidden if no active filters
|
||||
this.isVisible = false;
|
||||
return;
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the filter.
|
||||
* @param isVisible - The visibility to set.
|
||||
*/
|
||||
toggleVisibility: IFilterInstanceHelper<P, E>["toggleVisibility"] = action((isVisible) => {
|
||||
if (isVisible !== undefined) {
|
||||
this.isVisible = isVisible;
|
||||
return;
|
||||
}
|
||||
this.isVisible = !this.isVisible;
|
||||
});
|
||||
|
||||
// ------------ condition operations ------------
|
||||
|
||||
/**
|
||||
|
||||
@ -4,6 +4,7 @@ import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import {
|
||||
DEFAULT_FILTER_VISIBILITY_OPTIONS,
|
||||
TClearFilterOptions,
|
||||
TExpressionOptions,
|
||||
TFilterOptions,
|
||||
@ -71,6 +72,7 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
|
||||
// computed
|
||||
hasActiveFilters: boolean;
|
||||
hasChanges: boolean;
|
||||
isVisible: boolean;
|
||||
allConditions: TFilterConditionNode<P, TFilterValue>[];
|
||||
allConditionsForDisplay: TFilterConditionNodeForDisplay<P, TFilterValue>[];
|
||||
// computed option helpers
|
||||
@ -81,6 +83,8 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
|
||||
canClearFilters: boolean;
|
||||
canSaveView: boolean;
|
||||
canUpdateView: boolean;
|
||||
// visibility
|
||||
toggleVisibility: (isVisible?: boolean) => void;
|
||||
// filter expression actions
|
||||
resetExpression: (externalExpression: E, shouldResetInitialExpression?: boolean) => void;
|
||||
// filter condition
|
||||
@ -108,7 +112,7 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
|
||||
updateExpressionOptions: (newOptions: Partial<TExpressionOptions<E>>) => void;
|
||||
}
|
||||
|
||||
export type TFilterParams<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
type TFilterParams<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
adapter: IFilterAdapter<P, E>;
|
||||
options?: Partial<TFilterOptions<E>>;
|
||||
initialExpression?: E;
|
||||
@ -131,7 +135,9 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
||||
constructor(params: TFilterParams<P, E>) {
|
||||
this.id = uuidv4();
|
||||
this.adapter = params.adapter;
|
||||
this.helper = new FilterInstanceHelper<P, E>(this.adapter);
|
||||
this.helper = new FilterInstanceHelper<P, E>(this, {
|
||||
adapter: this.adapter,
|
||||
});
|
||||
this.configManager = new FilterConfigManager<P, E>(this, {
|
||||
options: params.options?.config,
|
||||
});
|
||||
@ -141,6 +147,7 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
||||
this.expression = cloneDeep(initialExpression);
|
||||
this.expressionOptions = this.helper.initializeExpressionOptions(params.options?.expression);
|
||||
this.onExpressionChange = params.onExpressionChange;
|
||||
this.helper.setInitialVisibility(params.options?.visibility ?? DEFAULT_FILTER_VISIBILITY_OPTIONS);
|
||||
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
@ -153,6 +160,7 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
||||
// computed
|
||||
hasActiveFilters: computed,
|
||||
hasChanges: computed,
|
||||
isVisible: computed,
|
||||
allConditions: computed,
|
||||
allConditionsForDisplay: computed,
|
||||
// computed option helpers
|
||||
@ -201,6 +209,14 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
||||
return !deepCompareFilterExpressions(this.initialFilterExpression, this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visibility of the filter instance.
|
||||
* @returns The visibility of the filter instance.
|
||||
*/
|
||||
get isVisible(): IFilterInstance<P, E>["isVisible"] {
|
||||
return this.helper.isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all conditions from the filter expression.
|
||||
* @returns An array of filter conditions.
|
||||
@ -279,6 +295,14 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
||||
|
||||
// ------------ actions ------------
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the filter instance.
|
||||
* @param isVisible - The visibility to set.
|
||||
*/
|
||||
toggleVisibility: IFilterInstance<P, E>["toggleVisibility"] = action((isVisible) => {
|
||||
this.helper.toggleVisibility(isVisible);
|
||||
});
|
||||
|
||||
/**
|
||||
* Resets the filter expression to the initial expression.
|
||||
* @param externalExpression - The external expression to reset to.
|
||||
|
||||
@ -6,10 +6,12 @@ import { EIssuesStoreType, LOGICAL_OPERATOR, TWorkItemFilterExpression, TWorkIte
|
||||
import { getOperatorForPayload } from "@plane/utils";
|
||||
// local imports
|
||||
import { buildWorkItemFilterExpressionFromConditions, TWorkItemFilterCondition } from "../../utils";
|
||||
import { FilterInstance, IFilterInstance } from "../rich-filters/filter";
|
||||
import { FilterInstance } from "../rich-filters/filter";
|
||||
import { workItemFiltersAdapter } from "./adapter";
|
||||
import { IWorkItemFilterInstance, TWorkItemFilterKey } from "./shared";
|
||||
|
||||
type TGetOrCreateFilterParams = {
|
||||
showOnMount?: boolean;
|
||||
entityId: string;
|
||||
entityType: EIssuesStoreType;
|
||||
expressionOptions?: TExpressionOptions<TWorkItemFilterExpression>;
|
||||
@ -17,17 +19,10 @@ type TGetOrCreateFilterParams = {
|
||||
onExpressionChange?: (expression: TWorkItemFilterExpression) => void;
|
||||
};
|
||||
|
||||
type TWorkItemFilterKey = `${EIssuesStoreType}-${string}`;
|
||||
|
||||
export interface IWorkItemFilterStore {
|
||||
filters: Map<TWorkItemFilterKey, IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>>; // key is the entity id (project, cycle, workspace, teamspace, etc)
|
||||
getFilter: (
|
||||
entityType: EIssuesStoreType,
|
||||
entityId: string
|
||||
) => IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined;
|
||||
getOrCreateFilter: (
|
||||
params: TGetOrCreateFilterParams
|
||||
) => IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>;
|
||||
filters: Map<TWorkItemFilterKey, IWorkItemFilterInstance>; // key is the entity id (project, cycle, workspace, teamspace, etc)
|
||||
getFilter: (entityType: EIssuesStoreType, entityId: string) => IWorkItemFilterInstance | undefined;
|
||||
getOrCreateFilter: (params: TGetOrCreateFilterParams) => IWorkItemFilterInstance;
|
||||
resetExpression: (entityType: EIssuesStoreType, entityId: string, expression: TWorkItemFilterExpression) => void;
|
||||
updateFilterExpressionFromConditions: (
|
||||
entityType: EIssuesStoreType,
|
||||
@ -48,7 +43,7 @@ export class WorkItemFilterStore implements IWorkItemFilterStore {
|
||||
filters: IWorkItemFilterStore["filters"];
|
||||
|
||||
constructor() {
|
||||
this.filters = new Map<TWorkItemFilterKey, IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>>();
|
||||
this.filters = new Map<TWorkItemFilterKey, IWorkItemFilterInstance>();
|
||||
makeObservable(this, {
|
||||
filters: observable,
|
||||
getOrCreateFilter: action,
|
||||
@ -87,12 +82,17 @@ export class WorkItemFilterStore implements IWorkItemFilterStore {
|
||||
if (params.onExpressionChange) {
|
||||
existingFilter.onExpressionChange = params.onExpressionChange;
|
||||
}
|
||||
// Update visibility if provided
|
||||
if (params.showOnMount !== undefined) {
|
||||
existingFilter.toggleVisibility(params.showOnMount);
|
||||
}
|
||||
return existingFilter;
|
||||
}
|
||||
|
||||
// create new filter instance
|
||||
const newFilter = this._initializeFilterInstance(params);
|
||||
this.filters.set(this._getFilterKey(params.entityType, params.entityId), newFilter);
|
||||
const filterKey = this._getFilterKey(params.entityType, params.entityId);
|
||||
this.filters.set(filterKey, newFilter);
|
||||
|
||||
return newFilter;
|
||||
});
|
||||
@ -210,6 +210,9 @@ export class WorkItemFilterStore implements IWorkItemFilterStore {
|
||||
onExpressionChange: params.onExpressionChange,
|
||||
options: {
|
||||
expression: params.expressionOptions,
|
||||
visibility: params.showOnMount
|
||||
? { autoSetVisibility: false, isVisibleOnMount: true }
|
||||
: { autoSetVisibility: true },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./adapter";
|
||||
export * from "./filter.store";
|
||||
export * from "./shared";
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
// plane imports
|
||||
import { EIssuesStoreType, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { IFilterInstance } from "../rich-filters";
|
||||
|
||||
export type TWorkItemFilterKey = `${EIssuesStoreType}-${string}`;
|
||||
|
||||
export type IWorkItemFilterInstance = IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>;
|
||||
Loading…
Reference in New Issue
Block a user