[WEB-5804] refactor: decouple filter value types from filter configurations (#8441)

* [WEB-5804] refactor: decouple filter value types from filter configurations

Remove value type constraints from filter configurations to support
operator-specific value types. Different operators can accept different
value types for the same filter property, so value types should be
determined at the operator level rather than the filter level.

- Remove generic value type parameter from TFilterConfig
- Update TOperatorConfigMap to accept union of all value types
- Simplify filter config factory signatures across all filter types
- Add forceUpdate parameter to updateConditionValue method

* refactor: remove filter value type constraints from filter configurations

Eliminate the generic value type parameter from filter configurations to allow for operator-specific value types. This change enhances flexibility by enabling different operators to accept various value types for the same filter property.

- Updated TFilterConfig and related interfaces to remove value type constraints
- Adjusted filter configuration methods and types accordingly
- Refactored date operator support to align with the new structure
This commit is contained in:
Prateek Shourya 2025-12-24 21:03:22 +05:30 committed by GitHub
parent 5499e49b72
commit f04be48f61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 126 additions and 133 deletions

View file

@ -3,7 +3,7 @@ import { computedFn } from "mobx-utils";
// plane imports
import type { TConfigOptions } from "@plane/constants";
import { DEFAULT_FILTER_CONFIG_OPTIONS } from "@plane/constants";
import type { TExternalFilter, TFilterConfig, TFilterProperty, TFilterValue } from "@plane/types";
import type { TExternalFilter, TFilterConfig, TFilterProperty } from "@plane/types";
// local imports
import type { IFilterConfig } from "./config";
import { FilterConfig } from "./config";
@ -24,17 +24,17 @@ import type { IFilterInstance } from "./filter";
*/
export interface IFilterConfigManager<P extends TFilterProperty> {
// observables
filterConfigs: Map<P, IFilterConfig<P, TFilterValue>>; // filter property -> config
filterConfigs: Map<P, IFilterConfig<P>>; // filter property -> config
configOptions: TConfigOptions;
areConfigsReady: boolean;
// computed
allAvailableConfigs: IFilterConfig<P, TFilterValue>[];
allAvailableConfigs: IFilterConfig<P>[];
// computed functions
getConfigByProperty: (property: P) => IFilterConfig<P, TFilterValue> | undefined;
getConfigByProperty: (property: P) => IFilterConfig<P> | undefined;
// helpers
register: <C extends TFilterConfig<P, TFilterValue>>(config: C) => void;
registerAll: (configs: TFilterConfig<P, TFilterValue>[]) => void;
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P, TFilterValue>>) => void;
register: <C extends TFilterConfig<P>>(config: C) => void;
registerAll: (configs: TFilterConfig<P>[]) => void;
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P>>) => void;
setAreConfigsReady: (value: boolean) => void;
}
@ -115,7 +115,7 @@ export class FilterConfigManager<
* @returns The config for the property, or undefined if not found.
*/
getConfigByProperty: IFilterConfigManager<P>["getConfigByProperty"] = computedFn(
(property) => this.filterConfigs.get(property) as IFilterConfig<P, TFilterValue>
(property) => this.filterConfigs.get(property) as IFilterConfig<P>
);
// ------------ helpers ------------
@ -165,7 +165,7 @@ export class FilterConfigManager<
// ------------ private computed ------------
private get _allConfigs(): IFilterConfig<P, TFilterValue>[] {
private get _allConfigs(): IFilterConfig<P>[] {
return Array.from(this.filterConfigs.values());
}
@ -173,7 +173,7 @@ export class FilterConfigManager<
* Returns all enabled filterConfigs.
* @returns All enabled filterConfigs.
*/
private get _allEnabledConfigs(): IFilterConfig<P, TFilterValue>[] {
private get _allEnabledConfigs(): IFilterConfig<P>[] {
return this._allConfigs.filter((config) => config.isEnabled);
}

View file

@ -25,41 +25,35 @@ type TOperatorOptionForDisplay = {
label: string;
};
export interface IFilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue> extends TFilterConfig<
P,
V
> {
export interface IFilterConfig<P extends TFilterProperty> extends TFilterConfig<P> {
// computed
allEnabledSupportedOperators: TSupportedOperators[];
firstOperator: TSupportedOperators | undefined;
// computed functions
getOperatorConfig: (
operator: TAllAvailableOperatorsForDisplay
) => TOperatorSpecificConfigs<V>[keyof TOperatorSpecificConfigs<V>] | undefined;
) => TOperatorSpecificConfigs[keyof TOperatorSpecificConfigs] | undefined;
getLabelForOperator: (operator: TAllAvailableOperatorsForDisplay | undefined) => string;
getDisplayOperatorByValue: <T extends TSupportedOperators>(operator: T, value: V) => T;
getAllDisplayOperatorOptionsByValue: (value: V) => TOperatorOptionForDisplay[];
getDisplayOperatorByValue: <T extends TSupportedOperators>(operator: T, value: TFilterValue) => T;
getAllDisplayOperatorOptionsByValue: (value: TFilterValue) => TOperatorOptionForDisplay[];
// actions
mutate: (updates: Partial<TFilterConfig<P, V>>) => void;
mutate: (updates: Partial<TFilterConfig<P>>) => void;
}
export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue> implements IFilterConfig<
P,
V
> {
export class FilterConfig<P extends TFilterProperty> implements IFilterConfig<P> {
// observables
id: IFilterConfig<P, V>["id"];
label: IFilterConfig<P, V>["label"];
icon?: IFilterConfig<P, V>["icon"];
isEnabled: IFilterConfig<P, V>["isEnabled"];
supportedOperatorConfigsMap: IFilterConfig<P, V>["supportedOperatorConfigsMap"];
allowMultipleFilters: IFilterConfig<P, V>["allowMultipleFilters"];
id: IFilterConfig<P>["id"];
label: IFilterConfig<P>["label"];
icon?: IFilterConfig<P>["icon"];
isEnabled: IFilterConfig<P>["isEnabled"];
supportedOperatorConfigsMap: IFilterConfig<P>["supportedOperatorConfigsMap"];
allowMultipleFilters: IFilterConfig<P>["allowMultipleFilters"];
/**
* Creates a new FilterConfig instance.
* @param params - The parameters for the filter config.
*/
constructor(params: TFilterConfig<P, V>) {
constructor(params: TFilterConfig<P>) {
this.id = params.id;
this.label = params.label;
this.icon = params.icon;
@ -88,7 +82,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* Returns all supported operators.
* @returns All supported operators.
*/
get allEnabledSupportedOperators(): IFilterConfig<P, V>["allEnabledSupportedOperators"] {
get allEnabledSupportedOperators(): IFilterConfig<P>["allEnabledSupportedOperators"] {
return Array.from(this.supportedOperatorConfigsMap.entries())
.filter(([, operatorConfig]) => operatorConfig.isOperatorEnabled)
.map(([operator]) => operator);
@ -98,7 +92,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* Returns the first operator.
* @returns The first operator.
*/
get firstOperator(): IFilterConfig<P, V>["firstOperator"] {
get firstOperator(): IFilterConfig<P>["firstOperator"] {
return this.allEnabledSupportedOperators[0];
}
@ -109,7 +103,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* @param operator - The operator.
* @returns The operator config.
*/
getOperatorConfig: IFilterConfig<P, V>["getOperatorConfig"] = computedFn((operator) =>
getOperatorConfig: IFilterConfig<P>["getOperatorConfig"] = computedFn((operator) =>
this.supportedOperatorConfigsMap.get(getOperatorForPayload(operator).operator)
);
@ -118,7 +112,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* @param operator - The operator.
* @returns The label for the operator.
*/
getLabelForOperator: IFilterConfig<P, V>["getLabelForOperator"] = computedFn((operator) => {
getLabelForOperator: IFilterConfig<P>["getLabelForOperator"] = computedFn((operator) => {
if (!operator) return EMPTY_OPERATOR_LABEL;
const operatorConfig = this.getOperatorConfig(operator);
@ -139,7 +133,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* @param value - The value.
* @returns The operator for the value.
*/
getDisplayOperatorByValue: IFilterConfig<P, V>["getDisplayOperatorByValue"] = computedFn((operator, value) => {
getDisplayOperatorByValue: IFilterConfig<P>["getDisplayOperatorByValue"] = computedFn((operator, value) => {
const operatorConfig = this.getOperatorConfig(operator);
if (operatorConfig?.type === FILTER_FIELD_TYPE.MULTI_SELECT && (Array.isArray(value) ? value.length : 0) <= 1) {
return operatorConfig.singleValueOperator as typeof operator;
@ -155,28 +149,26 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* @param value - The current filter value used to determine the appropriate operator variant
* @returns Array of operator options with their display labels and values
*/
getAllDisplayOperatorOptionsByValue: IFilterConfig<P, V>["getAllDisplayOperatorOptionsByValue"] = computedFn(
(value) => {
const operatorOptions: TOperatorOptionForDisplay[] = [];
getAllDisplayOperatorOptionsByValue: IFilterConfig<P>["getAllDisplayOperatorOptionsByValue"] = computedFn((value) => {
const operatorOptions: TOperatorOptionForDisplay[] = [];
// Process each supported operator to build display options
for (const operator of this.allEnabledSupportedOperators) {
const displayOperator = this.getDisplayOperatorByValue(operator, value);
const displayOperatorLabel = this.getLabelForOperator(displayOperator);
operatorOptions.push({
value: operator,
label: displayOperatorLabel,
});
// Process each supported operator to build display options
for (const operator of this.allEnabledSupportedOperators) {
const displayOperator = this.getDisplayOperatorByValue(operator, value);
const displayOperatorLabel = this.getLabelForOperator(displayOperator);
operatorOptions.push({
value: operator,
label: displayOperatorLabel,
});
const additionalOperatorOption = this._getAdditionalOperatorOptions(operator, value);
if (additionalOperatorOption) {
operatorOptions.push(additionalOperatorOption);
}
const additionalOperatorOption = this._getAdditionalOperatorOptions(operator, value);
if (additionalOperatorOption) {
operatorOptions.push(additionalOperatorOption);
}
return operatorOptions;
}
);
return operatorOptions;
});
// ------------ actions ------------
@ -184,11 +176,11 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* Mutates the config.
* @param updates - The updates to apply to the config.
*/
mutate: IFilterConfig<P, V>["mutate"] = action((updates) => {
mutate: IFilterConfig<P>["mutate"] = action((updates) => {
runInAction(() => {
for (const key in updates) {
if (updates.hasOwnProperty(key)) {
const configKey = key as keyof TFilterConfig<P, V>;
const configKey = key as keyof TFilterConfig<P>;
set(this, configKey, updates[configKey]);
}
}
@ -199,6 +191,6 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
private _getAdditionalOperatorOptions = (
_operator: TSupportedOperators,
_value: V
_value: TFilterValue
): TOperatorOptionForDisplay | undefined => undefined;
}

View file

@ -110,7 +110,11 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
isNegation: boolean
) => void;
updateConditionOperator: (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => void;
updateConditionValue: <V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => void;
updateConditionValue: <V extends TFilterValue>(
conditionId: string,
value: SingleOrArray<V>,
forceUpdate?: boolean
) => void;
removeCondition: (conditionId: string) => void;
// config actions
clearFilters: () => Promise<void>;
@ -439,9 +443,10 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
* Updates the value of a condition in the filter expression with automatic optimization.
* @param conditionId - The id of the condition to update.
* @param value - The new value for the condition.
* @param forceUpdate - Whether to force the update even if the value is the same as the condition before update.
*/
updateConditionValue: IFilterInstance<P, E>["updateConditionValue"] = action(
<V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => {
<V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>, forceUpdate: boolean = false) => {
// If the expression is not valid, return
if (!this.expression) return;
@ -458,7 +463,7 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
}
// If the value is the same as the condition before update, return
if (isEqual(conditionBeforeUpdate.value, value)) {
if (!forceUpdate && isEqual(conditionBeforeUpdate.value, value)) {
return;
}