Skip to content

Commit f04be48

Browse files
[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
1 parent 5499e49 commit f04be48

File tree

20 files changed

+128
-135
lines changed

20 files changed

+128
-135
lines changed

apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useCallback, useMemo } from "react";
2-
import { AtSign, Briefcase, Calendar } from "lucide-react";
2+
import { AtSign, Briefcase } from "lucide-react";
33
// plane imports
44
import { Logo } from "@plane/propel/emoji-icon-picker";
55
import {
6+
CalendarLayoutIcon,
67
CycleGroupIcon,
78
CycleIcon,
89
ModuleIcon,
@@ -21,7 +22,6 @@ import type {
2122
IState,
2223
IUserLite,
2324
TFilterConfig,
24-
TFilterValue,
2525
IIssueLabel,
2626
IModule,
2727
IProject,
@@ -74,9 +74,9 @@ export type TUseWorkItemFiltersConfigProps = {
7474

7575
export type TWorkItemFiltersConfig = {
7676
areAllConfigsInitialized: boolean;
77-
configs: TFilterConfig<TWorkItemFilterProperty, TFilterValue>[];
77+
configs: TFilterConfig<TWorkItemFilterProperty>[];
7878
configMap: {
79-
[key in TWorkItemFilterProperty]?: TFilterConfig<TWorkItemFilterProperty, TFilterValue>;
79+
[key in TWorkItemFilterProperty]?: TFilterConfig<TWorkItemFilterProperty>;
8080
};
8181
isFilterEnabled: (key: TWorkItemFilterProperty) => boolean;
8282
members: IUserLite[];
@@ -326,7 +326,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
326326
() =>
327327
getCreatedAtFilterConfig<TWorkItemFilterProperty>("created_at")({
328328
isEnabled: true,
329-
filterIcon: Calendar,
329+
filterIcon: CalendarLayoutIcon,
330330
...operatorConfigs,
331331
}),
332332
[operatorConfigs]
@@ -337,7 +337,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
337337
() =>
338338
getUpdatedAtFilterConfig<TWorkItemFilterProperty>("updated_at")({
339339
isEnabled: true,
340-
filterIcon: Calendar,
340+
filterIcon: CalendarLayoutIcon,
341341
...operatorConfigs,
342342
}),
343343
[operatorConfigs]

packages/shared-state/src/store/rich-filters/config-manager.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { computedFn } from "mobx-utils";
33
// plane imports
44
import type { TConfigOptions } from "@plane/constants";
55
import { DEFAULT_FILTER_CONFIG_OPTIONS } from "@plane/constants";
6-
import type { TExternalFilter, TFilterConfig, TFilterProperty, TFilterValue } from "@plane/types";
6+
import type { TExternalFilter, TFilterConfig, TFilterProperty } from "@plane/types";
77
// local imports
88
import type { IFilterConfig } from "./config";
99
import { FilterConfig } from "./config";
@@ -24,17 +24,17 @@ import type { IFilterInstance } from "./filter";
2424
*/
2525
export interface IFilterConfigManager<P extends TFilterProperty> {
2626
// observables
27-
filterConfigs: Map<P, IFilterConfig<P, TFilterValue>>; // filter property -> config
27+
filterConfigs: Map<P, IFilterConfig<P>>; // filter property -> config
2828
configOptions: TConfigOptions;
2929
areConfigsReady: boolean;
3030
// computed
31-
allAvailableConfigs: IFilterConfig<P, TFilterValue>[];
31+
allAvailableConfigs: IFilterConfig<P>[];
3232
// computed functions
33-
getConfigByProperty: (property: P) => IFilterConfig<P, TFilterValue> | undefined;
33+
getConfigByProperty: (property: P) => IFilterConfig<P> | undefined;
3434
// helpers
35-
register: <C extends TFilterConfig<P, TFilterValue>>(config: C) => void;
36-
registerAll: (configs: TFilterConfig<P, TFilterValue>[]) => void;
37-
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P, TFilterValue>>) => void;
35+
register: <C extends TFilterConfig<P>>(config: C) => void;
36+
registerAll: (configs: TFilterConfig<P>[]) => void;
37+
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P>>) => void;
3838
setAreConfigsReady: (value: boolean) => void;
3939
}
4040

@@ -115,7 +115,7 @@ export class FilterConfigManager<
115115
* @returns The config for the property, or undefined if not found.
116116
*/
117117
getConfigByProperty: IFilterConfigManager<P>["getConfigByProperty"] = computedFn(
118-
(property) => this.filterConfigs.get(property) as IFilterConfig<P, TFilterValue>
118+
(property) => this.filterConfigs.get(property) as IFilterConfig<P>
119119
);
120120

121121
// ------------ helpers ------------
@@ -165,15 +165,15 @@ export class FilterConfigManager<
165165

166166
// ------------ private computed ------------
167167

168-
private get _allConfigs(): IFilterConfig<P, TFilterValue>[] {
168+
private get _allConfigs(): IFilterConfig<P>[] {
169169
return Array.from(this.filterConfigs.values());
170170
}
171171

172172
/**
173173
* Returns all enabled filterConfigs.
174174
* @returns All enabled filterConfigs.
175175
*/
176-
private get _allEnabledConfigs(): IFilterConfig<P, TFilterValue>[] {
176+
private get _allEnabledConfigs(): IFilterConfig<P>[] {
177177
return this._allConfigs.filter((config) => config.isEnabled);
178178
}
179179

packages/shared-state/src/store/rich-filters/config.ts

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,35 @@ type TOperatorOptionForDisplay = {
2525
label: string;
2626
};
2727

28-
export interface IFilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue> extends TFilterConfig<
29-
P,
30-
V
31-
> {
28+
export interface IFilterConfig<P extends TFilterProperty> extends TFilterConfig<P> {
3229
// computed
3330
allEnabledSupportedOperators: TSupportedOperators[];
3431
firstOperator: TSupportedOperators | undefined;
3532
// computed functions
3633
getOperatorConfig: (
3734
operator: TAllAvailableOperatorsForDisplay
38-
) => TOperatorSpecificConfigs<V>[keyof TOperatorSpecificConfigs<V>] | undefined;
35+
) => TOperatorSpecificConfigs[keyof TOperatorSpecificConfigs] | undefined;
3936
getLabelForOperator: (operator: TAllAvailableOperatorsForDisplay | undefined) => string;
40-
getDisplayOperatorByValue: <T extends TSupportedOperators>(operator: T, value: V) => T;
41-
getAllDisplayOperatorOptionsByValue: (value: V) => TOperatorOptionForDisplay[];
37+
getDisplayOperatorByValue: <T extends TSupportedOperators>(operator: T, value: TFilterValue) => T;
38+
getAllDisplayOperatorOptionsByValue: (value: TFilterValue) => TOperatorOptionForDisplay[];
4239
// actions
43-
mutate: (updates: Partial<TFilterConfig<P, V>>) => void;
40+
mutate: (updates: Partial<TFilterConfig<P>>) => void;
4441
}
4542

46-
export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue> implements IFilterConfig<
47-
P,
48-
V
49-
> {
43+
export class FilterConfig<P extends TFilterProperty> implements IFilterConfig<P> {
5044
// observables
51-
id: IFilterConfig<P, V>["id"];
52-
label: IFilterConfig<P, V>["label"];
53-
icon?: IFilterConfig<P, V>["icon"];
54-
isEnabled: IFilterConfig<P, V>["isEnabled"];
55-
supportedOperatorConfigsMap: IFilterConfig<P, V>["supportedOperatorConfigsMap"];
56-
allowMultipleFilters: IFilterConfig<P, V>["allowMultipleFilters"];
45+
id: IFilterConfig<P>["id"];
46+
label: IFilterConfig<P>["label"];
47+
icon?: IFilterConfig<P>["icon"];
48+
isEnabled: IFilterConfig<P>["isEnabled"];
49+
supportedOperatorConfigsMap: IFilterConfig<P>["supportedOperatorConfigsMap"];
50+
allowMultipleFilters: IFilterConfig<P>["allowMultipleFilters"];
5751

5852
/**
5953
* Creates a new FilterConfig instance.
6054
* @param params - The parameters for the filter config.
6155
*/
62-
constructor(params: TFilterConfig<P, V>) {
56+
constructor(params: TFilterConfig<P>) {
6357
this.id = params.id;
6458
this.label = params.label;
6559
this.icon = params.icon;
@@ -88,7 +82,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
8882
* Returns all supported operators.
8983
* @returns All supported operators.
9084
*/
91-
get allEnabledSupportedOperators(): IFilterConfig<P, V>["allEnabledSupportedOperators"] {
85+
get allEnabledSupportedOperators(): IFilterConfig<P>["allEnabledSupportedOperators"] {
9286
return Array.from(this.supportedOperatorConfigsMap.entries())
9387
.filter(([, operatorConfig]) => operatorConfig.isOperatorEnabled)
9488
.map(([operator]) => operator);
@@ -98,7 +92,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
9892
* Returns the first operator.
9993
* @returns The first operator.
10094
*/
101-
get firstOperator(): IFilterConfig<P, V>["firstOperator"] {
95+
get firstOperator(): IFilterConfig<P>["firstOperator"] {
10296
return this.allEnabledSupportedOperators[0];
10397
}
10498

@@ -109,7 +103,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
109103
* @param operator - The operator.
110104
* @returns The operator config.
111105
*/
112-
getOperatorConfig: IFilterConfig<P, V>["getOperatorConfig"] = computedFn((operator) =>
106+
getOperatorConfig: IFilterConfig<P>["getOperatorConfig"] = computedFn((operator) =>
113107
this.supportedOperatorConfigsMap.get(getOperatorForPayload(operator).operator)
114108
);
115109

@@ -118,7 +112,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
118112
* @param operator - The operator.
119113
* @returns The label for the operator.
120114
*/
121-
getLabelForOperator: IFilterConfig<P, V>["getLabelForOperator"] = computedFn((operator) => {
115+
getLabelForOperator: IFilterConfig<P>["getLabelForOperator"] = computedFn((operator) => {
122116
if (!operator) return EMPTY_OPERATOR_LABEL;
123117

124118
const operatorConfig = this.getOperatorConfig(operator);
@@ -139,7 +133,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
139133
* @param value - The value.
140134
* @returns The operator for the value.
141135
*/
142-
getDisplayOperatorByValue: IFilterConfig<P, V>["getDisplayOperatorByValue"] = computedFn((operator, value) => {
136+
getDisplayOperatorByValue: IFilterConfig<P>["getDisplayOperatorByValue"] = computedFn((operator, value) => {
143137
const operatorConfig = this.getOperatorConfig(operator);
144138
if (operatorConfig?.type === FILTER_FIELD_TYPE.MULTI_SELECT && (Array.isArray(value) ? value.length : 0) <= 1) {
145139
return operatorConfig.singleValueOperator as typeof operator;
@@ -155,40 +149,38 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
155149
* @param value - The current filter value used to determine the appropriate operator variant
156150
* @returns Array of operator options with their display labels and values
157151
*/
158-
getAllDisplayOperatorOptionsByValue: IFilterConfig<P, V>["getAllDisplayOperatorOptionsByValue"] = computedFn(
159-
(value) => {
160-
const operatorOptions: TOperatorOptionForDisplay[] = [];
161-
162-
// Process each supported operator to build display options
163-
for (const operator of this.allEnabledSupportedOperators) {
164-
const displayOperator = this.getDisplayOperatorByValue(operator, value);
165-
const displayOperatorLabel = this.getLabelForOperator(displayOperator);
166-
operatorOptions.push({
167-
value: operator,
168-
label: displayOperatorLabel,
169-
});
170-
171-
const additionalOperatorOption = this._getAdditionalOperatorOptions(operator, value);
172-
if (additionalOperatorOption) {
173-
operatorOptions.push(additionalOperatorOption);
174-
}
152+
getAllDisplayOperatorOptionsByValue: IFilterConfig<P>["getAllDisplayOperatorOptionsByValue"] = computedFn((value) => {
153+
const operatorOptions: TOperatorOptionForDisplay[] = [];
154+
155+
// Process each supported operator to build display options
156+
for (const operator of this.allEnabledSupportedOperators) {
157+
const displayOperator = this.getDisplayOperatorByValue(operator, value);
158+
const displayOperatorLabel = this.getLabelForOperator(displayOperator);
159+
operatorOptions.push({
160+
value: operator,
161+
label: displayOperatorLabel,
162+
});
163+
164+
const additionalOperatorOption = this._getAdditionalOperatorOptions(operator, value);
165+
if (additionalOperatorOption) {
166+
operatorOptions.push(additionalOperatorOption);
175167
}
176-
177-
return operatorOptions;
178168
}
179-
);
169+
170+
return operatorOptions;
171+
});
180172

181173
// ------------ actions ------------
182174

183175
/**
184176
* Mutates the config.
185177
* @param updates - The updates to apply to the config.
186178
*/
187-
mutate: IFilterConfig<P, V>["mutate"] = action((updates) => {
179+
mutate: IFilterConfig<P>["mutate"] = action((updates) => {
188180
runInAction(() => {
189181
for (const key in updates) {
190182
if (updates.hasOwnProperty(key)) {
191-
const configKey = key as keyof TFilterConfig<P, V>;
183+
const configKey = key as keyof TFilterConfig<P>;
192184
set(this, configKey, updates[configKey]);
193185
}
194186
}
@@ -199,6 +191,6 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
199191

200192
private _getAdditionalOperatorOptions = (
201193
_operator: TSupportedOperators,
202-
_value: V
194+
_value: TFilterValue
203195
): TOperatorOptionForDisplay | undefined => undefined;
204196
}

packages/shared-state/src/store/rich-filters/filter.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
110110
isNegation: boolean
111111
) => void;
112112
updateConditionOperator: (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => void;
113-
updateConditionValue: <V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => void;
113+
updateConditionValue: <V extends TFilterValue>(
114+
conditionId: string,
115+
value: SingleOrArray<V>,
116+
forceUpdate?: boolean
117+
) => void;
114118
removeCondition: (conditionId: string) => void;
115119
// config actions
116120
clearFilters: () => Promise<void>;
@@ -439,9 +443,10 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
439443
* Updates the value of a condition in the filter expression with automatic optimization.
440444
* @param conditionId - The id of the condition to update.
441445
* @param value - The new value for the condition.
446+
* @param forceUpdate - Whether to force the update even if the value is the same as the condition before update.
442447
*/
443448
updateConditionValue: IFilterInstance<P, E>["updateConditionValue"] = action(
444-
<V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => {
449+
<V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>, forceUpdate: boolean = false) => {
445450
// If the expression is not valid, return
446451
if (!this.expression) return;
447452

@@ -458,7 +463,7 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
458463
}
459464

460465
// If the value is the same as the condition before update, return
461-
if (isEqual(conditionBeforeUpdate.value, value)) {
466+
if (!forceUpdate && isEqual(conditionBeforeUpdate.value, value)) {
462467
return;
463468
}
464469

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TFilterProperty, TFilterValue } from "../expression";
1+
import type { TFilterProperty } from "../expression";
22
import type { TOperatorConfigMap } from "../operator-configs";
33

44
/**
@@ -8,13 +8,13 @@ import type { TOperatorConfigMap } from "../operator-configs";
88
* @template P - Property key type (e.g., 'state_id', 'priority', 'assignee')
99
* @template V - Value type for the filter
1010
*/
11-
export type TFilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue> = {
11+
export type TFilterConfig<P extends TFilterProperty> = {
1212
id: P;
1313
label: string;
1414
icon?: React.FC<React.SVGAttributes<SVGElement>>;
1515
isEnabled: boolean;
1616
allowMultipleFilters?: boolean;
17-
supportedOperatorConfigsMap: TOperatorConfigMap<V>;
17+
supportedOperatorConfigsMap: TOperatorConfigMap;
1818
rightContent?: React.ReactNode; // content to display on the right side of the filter option in the dropdown
1919
tooltipContent?: React.ReactNode; // content to display when hovering over the applied filter item in the filter list
2020
};

packages/types/src/rich-filters/derived/core.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ import type { TFilterOperatorHelper } from "./shared";
1414
* Union type representing all core operators that support single date filter types.
1515
*/
1616
export type TCoreSupportedSingleDateFilterOperators<V extends TFilterValue = TFilterValue> = {
17-
[K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
18-
TCoreOperatorSpecificConfigs<V>,
17+
[K in keyof TCoreOperatorSpecificConfigs]: TFilterOperatorHelper<
18+
TCoreOperatorSpecificConfigs,
1919
K,
2020
TDateFilterFieldConfig<V>
2121
>;
22-
}[keyof TCoreOperatorSpecificConfigs<V>];
22+
}[keyof TCoreOperatorSpecificConfigs];
2323

2424
/**
2525
* Union type representing all core operators that support range date filter types.
2626
*/
2727
export type TCoreSupportedRangeDateFilterOperators<V extends TFilterValue = TFilterValue> = {
28-
[K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
29-
TCoreOperatorSpecificConfigs<V>,
28+
[K in keyof TCoreOperatorSpecificConfigs]: TFilterOperatorHelper<
29+
TCoreOperatorSpecificConfigs,
3030
K,
3131
TDateRangeFilterFieldConfig<V>
3232
>;
33-
}[keyof TCoreOperatorSpecificConfigs<V>];
33+
}[keyof TCoreOperatorSpecificConfigs];
3434

3535
/**
3636
* Union type representing all core operators that support date filter types.
@@ -48,23 +48,23 @@ export type TCoreAllAvailableDateFilterOperatorsForDisplay<V extends TFilterValu
4848
* Union type representing all core operators that support single select filter types.
4949
*/
5050
export type TCoreSupportedSingleSelectFilterOperators<V extends TFilterValue = TFilterValue> = {
51-
[K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
52-
TCoreOperatorSpecificConfigs<V>,
51+
[K in keyof TCoreOperatorSpecificConfigs]: TFilterOperatorHelper<
52+
TCoreOperatorSpecificConfigs,
5353
K,
5454
TSingleSelectFilterFieldConfig<V>
5555
>;
56-
}[keyof TCoreOperatorSpecificConfigs<V>];
56+
}[keyof TCoreOperatorSpecificConfigs];
5757

5858
/**
5959
* Union type representing all core operators that support multi select filter types.
6060
*/
6161
export type TCoreSupportedMultiSelectFilterOperators<V extends TFilterValue = TFilterValue> = {
62-
[K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
63-
TCoreOperatorSpecificConfigs<V>,
62+
[K in keyof TCoreOperatorSpecificConfigs]: TFilterOperatorHelper<
63+
TCoreOperatorSpecificConfigs,
6464
K,
6565
TMultiSelectFilterFieldConfig<V>
6666
>;
67-
}[keyof TCoreOperatorSpecificConfigs<V>];
67+
}[keyof TCoreOperatorSpecificConfigs];
6868

6969
/**
7070
* Union type representing all core operators that support any select filter types.

0 commit comments

Comments
 (0)