[WEB-5099] improvement: enhance rich filters with new components and configurations (#7916)

* feat: enhance rich filters with new components and configurations

- Added `AdditionalFilterValueInput` for unsupported filter types.
- Introduced `FilterItem` and related components for better filter item management.
- Updated filter configurations to include new properties and support for multiple values.
- Improved loading states and error handling in filter components.
- Refactored existing filter logic to streamline operations and enhance performance.

* Refactor rich filters component structure and enhance filter item functionality

- Moved AddFilterButton and AddFilterDropdown to a new directory structure for better organization.
- Updated FilterItemProperty to handle filter selection and condition updates more effectively.
- Enhanced the FilterInstance class with methods to update condition properties and operators, improving filter management.
- Added new functionality to handle invalid filter states and improve user feedback.

* [WEB-5111] feat: add 'created_at' and 'updated_at' filters to work item configuration

- Introduced new filter configurations for 'created_at' and 'updated_at' in the work item filters.
- Updated relevant components to utilize these new filters, enhancing filtering capabilities.
- Added corresponding filter configuration functions in the utils for better date handling.

* fix: build
This commit is contained in:
Prateek Shourya 2025-10-14 01:39:24 +05:30 committed by GitHub
parent 9f41e92d21
commit cfb4a8212c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 854 additions and 247 deletions

View file

@ -24,6 +24,7 @@ export interface IFilterConfigManager<P extends TFilterProperty> {
// observables
filterConfigs: Map<P, IFilterConfig<P, TFilterValue>>; // filter property -> config
configOptions: TConfigOptions;
areConfigsReady: boolean;
// computed
allAvailableConfigs: IFilterConfig<P, TFilterValue>[];
// computed functions
@ -32,6 +33,7 @@ export interface IFilterConfigManager<P extends TFilterProperty> {
register: <C extends TFilterConfig<P, TFilterValue>>(config: C) => void;
registerAll: (configs: TFilterConfig<P, TFilterValue>[]) => void;
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P, TFilterValue>>) => void;
setAreConfigsReady: (value: boolean) => void;
}
/**
@ -57,6 +59,7 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
// observables
filterConfigs: IFilterConfigManager<P>["filterConfigs"];
configOptions: IFilterConfigManager<P>["configOptions"];
areConfigsReady: IFilterConfigManager<P>["areConfigsReady"];
// parent filter instance
private _filterInstance: IFilterInstance<P, E>;
@ -69,18 +72,21 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
constructor(filterInstance: IFilterInstance<P, E>, params: TConfigManagerParams) {
this.filterConfigs = new Map<P, IFilterConfig<P>>();
this.configOptions = this._initializeConfigOptions(params.options);
this.areConfigsReady = true;
// parent filter instance
this._filterInstance = filterInstance;
makeObservable(this, {
filterConfigs: observable,
configOptions: observable,
areConfigsReady: observable,
// computed
allAvailableConfigs: computed,
// helpers
register: action,
registerAll: action,
updateConfigByProperty: action,
setAreConfigsReady: action,
});
}
@ -146,6 +152,14 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
prevConfig?.mutate(configUpdates);
});
/**
* Updates the configs ready state.
* @param value - The new configs ready state.
*/
setAreConfigsReady: IFilterConfigManager<P>["setAreConfigsReady"] = action((value) => {
this.areConfigsReady = value;
});
// ------------ private computed ------------
private get _allConfigs(): IFilterConfig<P, TFilterValue>[] {

View file

@ -6,6 +6,7 @@ import {
IFilterAdapter,
LOGICAL_OPERATOR,
TSupportedOperators,
TFilterConditionNode,
TFilterExpression,
TFilterValue,
TFilterProperty,
@ -43,9 +44,16 @@ export interface IFilterInstanceHelper<P extends TFilterProperty, E extends TExt
condition: TFilterConditionPayload<P, V>,
isNegation: boolean
) => TFilterExpression<P> | null;
handleConditionPropertyUpdate: (
expression: TFilterExpression<P>,
conditionId: string,
property: P,
operator: TSupportedOperators,
isNegation: boolean
) => TFilterExpression<P> | null;
// group operations
restructureExpressionForOperatorChange: (
expression: TFilterExpression<P> | null,
expression: TFilterExpression<P>,
conditionId: string,
newOperator: TSupportedOperators,
isNegation: boolean,
@ -162,6 +170,28 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
isNegation
) => this._addConditionByOperator(expression, groupOperator, this._getConditionPayloadToAdd(condition, isNegation));
/**
* Updates the property and operator of a condition in the filter expression.
* This method updates the property, operator, resets the value, and handles negation properly.
* @param expression - The filter expression to operate on
* @param conditionId - The ID of the condition being updated
* @param property - The new property for the condition
* @param operator - The new operator for the condition
* @param isNegation - Whether the condition should be negated
* @returns The updated expression
*/
handleConditionPropertyUpdate: IFilterInstanceHelper<P, E>["handleConditionPropertyUpdate"] = (
expression,
conditionId,
property,
operator,
isNegation
) => {
const payload = { property, operator, value: undefined };
return this._updateCondition(expression, conditionId, payload, isNegation);
};
// ------------ group operations ------------
/**
@ -177,17 +207,12 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
expression,
conditionId,
newOperator,
_isNegation,
isNegation,
shouldResetValue
) => {
if (!expression) return null;
const payload = shouldResetValue ? { operator: newOperator, value: undefined } : { operator: newOperator };
// Update the condition with the new operator
updateNodeInExpression(expression, conditionId, payload);
return expression;
return this._updateCondition(expression, conditionId, payload, isNegation);
};
// ------------ private helpers ------------
@ -227,4 +252,24 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
return expression;
}
}
/**
* Updates a condition with the given payload and handles negation wrapping/unwrapping.
* @param expression - The filter expression to operate on
* @param conditionId - The ID of the condition being updated
* @param payload - The payload to update the condition with
* @param isNegation - Whether the condition should be negated
* @returns The updated expression with proper negation handling
*/
private _updateCondition = (
expression: TFilterExpression<P>,
conditionId: string,
payload: Partial<TFilterConditionNode<P, TFilterValue>>,
_isNegation: boolean
): TFilterExpression<P> | null => {
// Update the condition with the payload
updateNodeInExpression(expression, conditionId, payload);
return expression;
};
}

View file

@ -1,4 +1,4 @@
import { cloneDeep } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import { action, computed, makeObservable, observable, toJS } from "mobx";
import { computedFn } from "mobx-utils";
import { v4 as uuidv4 } from "uuid";
@ -101,6 +101,12 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
condition: TFilterConditionPayload<P, V>,
isNegation: boolean
) => void;
updateConditionProperty: (
conditionId: string,
property: P,
operator: TSupportedOperators,
isNegation: boolean
) => void;
updateConditionOperator: (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => void;
updateConditionValue: <V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => void;
removeCondition: (conditionId: string) => void;
@ -360,6 +366,33 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
}
});
/**
* Updates the property of a condition in the filter expression.
* @param conditionId - The id of the condition to update.
* @param property - The new property for the condition.
*/
updateConditionProperty: IFilterInstance<P, E>["updateConditionProperty"] = action(
(conditionId: string, property: P, operator: TSupportedOperators, isNegation: boolean) => {
if (!this.expression) return;
const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;
// Update the condition property
const updatedExpression = this.helper.handleConditionPropertyUpdate(
this.expression,
conditionId,
property,
operator,
isNegation
);
if (updatedExpression) {
this.expression = updatedExpression;
this._notifyExpressionChange();
}
}
);
/**
* Updates the operator of a condition in the filter expression.
* @param conditionId - The id of the condition to update.
@ -410,12 +443,23 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
// If the expression is not valid, return
if (!this.expression) return;
// Get the condition before update
const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
// If the condition is not valid, return
if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;
// If the value is not valid, remove the condition
if (!hasValidValue(value)) {
this.removeCondition(conditionId);
return;
}
// If the value is the same as the condition before update, return
if (isEqual(conditionBeforeUpdate.value, value)) {
return;
}
// Update the condition value
updateNodeInExpression(this.expression, conditionId, {
value,

View file

@ -2,6 +2,7 @@
import { isEmpty } from "lodash-es";
import {
LOGICAL_OPERATOR,
MULTI_VALUE_OPERATORS,
SingleOrArray,
TFilterExpression,
TFilterValue,
@ -161,7 +162,8 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
// Validate property is in allowed list
if (!WORK_ITEM_FILTER_PROPERTY_KEYS.includes(property as TWorkItemFilterProperty)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!WORK_ITEM_FILTER_PROPERTY_KEYS.includes(property as any) && !property.startsWith("customproperty_")) {
return false;
}
@ -192,17 +194,12 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
// Find the last occurrence of '__' to separate property from operator
const lastDoubleUnderscoreIndex = key.lastIndexOf("__");
const property = key.substring(0, lastDoubleUnderscoreIndex);
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
const operator = key.substring(lastDoubleUnderscoreIndex + 2) as TSupportedOperators;
const rawValue = data[key as TWorkItemFilterConditionKey];
if (typeof rawValue !== "string") {
console.error(`Filter value must be a string, got: ${typeof rawValue}`);
return null;
}
// Parse comma-separated values
const parsedValue = this._parseFilterValue(rawValue);
const parsedValue = MULTI_VALUE_OPERATORS.includes(operator) ? this._parseFilterValue(rawValue) : rawValue;
return [property as TWorkItemFilterProperty, operator as TSupportedOperators, parsedValue];
};
@ -212,7 +209,9 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
* @param value - The string value to parse
* @returns Parsed value as string or array of strings
*/
private _parseFilterValue = (value: string): SingleOrArray<TFilterValue> => {
private _parseFilterValue = (value: TFilterValue): SingleOrArray<TFilterValue> => {
if (!value) return value;
if (typeof value !== "string") return value;
// Handle empty string