[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

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