[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:
Prateek Shourya 2025-09-30 18:19:43 +05:30 committed by GitHub
parent 992457efd2
commit 7ce21a6488
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 394 additions and 141 deletions

View File

@ -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"

View File

@ -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>
</>

View File

@ -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={() => {

View File

@ -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

View File

@ -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 || {}}

View File

@ -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")}

View File

@ -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";

View File

@ -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

View File

@ -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";

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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={

View File

@ -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";

View File

@ -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,

View File

@ -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>
);
}
);

View 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>
);
}
);

View File

@ -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>

View File

@ -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

View File

@ -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;
};

View File

@ -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} />);

View File

@ -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} />;
});

View File

@ -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>

View File

@ -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);
};

View File

@ -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;
};

View File

@ -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.

View File

@ -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 ------------
/**

View File

@ -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.

View File

@ -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 },
},
});
}

View File

@ -1,2 +1,3 @@
export * from "./adapter";
export * from "./filter.store";
export * from "./shared";

View File

@ -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>;