[WEB-4885] feat: new filters architecture and UI components (#7802)
* feat: add rich filters types * feat: add rich filters constants * feat: add rich filters utils * feat: add rich filters store in shared state package * feat: add rich filters UI components * fix: make setLoading optional in loadOptions function for improved flexibility * chore: minor improvements to rich filters * fix: formatting
This commit is contained in:
parent
00e070b509
commit
d521eab22f
83 changed files with 4345 additions and 117 deletions
|
|
@ -15,13 +15,21 @@
|
|||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"lodash": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-utils": "catalog:",
|
||||
"uuid": "catalog:",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/uuid": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./store";
|
||||
export * from "./utils";
|
||||
1
packages/shared-state/src/store/index.ts
Normal file
1
packages/shared-state/src/store/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./rich-filters";
|
||||
31
packages/shared-state/src/store/rich-filters/adapter.ts
Normal file
31
packages/shared-state/src/store/rich-filters/adapter.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// plane imports
|
||||
import { IFilterAdapter, TExternalFilter, TFilterExpression, TFilterProperty } from "@plane/types";
|
||||
|
||||
/**
|
||||
* Abstract base class for converting between external filter formats and internal filter expressions.
|
||||
* Provides common utilities for creating and manipulating filter nodes.
|
||||
*
|
||||
* @template K - Property key type that extends TFilterProperty
|
||||
* @template E - External filter type that extends TExternalFilter
|
||||
*/
|
||||
export abstract class FilterAdapter<K extends TFilterProperty, E extends TExternalFilter>
|
||||
implements IFilterAdapter<K, E>
|
||||
{
|
||||
/**
|
||||
* Converts an external filter format to internal filter expression.
|
||||
* Must be implemented by concrete adapter classes.
|
||||
*
|
||||
* @param externalFilter - The external filter to convert
|
||||
* @returns The internal filter expression or null if conversion fails
|
||||
*/
|
||||
abstract toInternal(externalFilter: E): TFilterExpression<K> | null;
|
||||
|
||||
/**
|
||||
* Converts an internal filter expression to external filter format.
|
||||
* Must be implemented by concrete adapter classes.
|
||||
*
|
||||
* @param internalFilter - The internal filter expression to convert
|
||||
* @returns The external filter format
|
||||
*/
|
||||
abstract toExternal(internalFilter: TFilterExpression<K> | null): E;
|
||||
}
|
||||
173
packages/shared-state/src/store/rich-filters/config-manager.ts
Normal file
173
packages/shared-state/src/store/rich-filters/config-manager.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { DEFAULT_FILTER_CONFIG_OPTIONS, TConfigOptions } from "@plane/constants";
|
||||
import { TExternalFilter, TFilterConfig, TFilterProperty, TFilterValue } from "@plane/types";
|
||||
// local imports
|
||||
import { FilterConfig, IFilterConfig } from "./config";
|
||||
import { IFilterInstance } from "./filter";
|
||||
|
||||
/**
|
||||
* Interface for managing filter configurations.
|
||||
* Provides methods to register, update, and retrieve filter configurations.
|
||||
* - filterConfigs: Map storing filter configurations by their ID
|
||||
* - configOptions: Configuration options controlling filter behavior
|
||||
* - allConfigs: All registered filter configurations
|
||||
* - allAvailableConfigs: All available filter configurations based on current state
|
||||
* - getConfigByProperty: Retrieves a filter configuration by its ID
|
||||
* - register: Registers a single filter configuration
|
||||
* - registerAll: Registers multiple filter configurations
|
||||
* - updateConfigByProperty: Updates an existing filter configuration by ID
|
||||
* @template P - The filter property type extending TFilterProperty
|
||||
*/
|
||||
export interface IFilterConfigManager<P extends TFilterProperty> {
|
||||
// observables
|
||||
filterConfigs: Map<P, IFilterConfig<P, TFilterValue>>; // filter property -> config
|
||||
configOptions: TConfigOptions;
|
||||
// computed
|
||||
allAvailableConfigs: IFilterConfig<P, TFilterValue>[];
|
||||
// computed functions
|
||||
getConfigByProperty: (property: P) => IFilterConfig<P, TFilterValue> | 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for initializing the FilterConfigManager.
|
||||
* - options: Optional configuration options to override defaults
|
||||
*/
|
||||
export type TConfigManagerParams = {
|
||||
options?: Partial<TConfigOptions>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages filter configurations for a filter instance.
|
||||
* Handles registration, updates, and retrieval of filter configurations.
|
||||
* Provides computed properties for available configurations based on current filter state.
|
||||
*
|
||||
* @template P - The filter property type extending TFilterProperty
|
||||
* @template V - The filter value type extending TFilterValue
|
||||
* @template E - The external filter type extending TExternalFilter
|
||||
*/
|
||||
export class FilterConfigManager<P extends TFilterProperty, E extends TExternalFilter = TExternalFilter>
|
||||
implements IFilterConfigManager<P>
|
||||
{
|
||||
// observables
|
||||
filterConfigs: IFilterConfigManager<P>["filterConfigs"];
|
||||
configOptions: IFilterConfigManager<P>["configOptions"];
|
||||
// parent filter instance
|
||||
_filterInstance: IFilterInstance<P, E>;
|
||||
|
||||
/**
|
||||
* Creates a new FilterConfigManager instance.
|
||||
*
|
||||
* @param filterInstance - The parent filter instance this manager belongs to
|
||||
* @param params - Configuration parameters for the manager
|
||||
*/
|
||||
constructor(filterInstance: IFilterInstance<P, E>, params: TConfigManagerParams) {
|
||||
this.filterConfigs = new Map<P, IFilterConfig<P>>();
|
||||
this.configOptions = this._initializeConfigOptions(params.options);
|
||||
// parent filter instance
|
||||
this._filterInstance = filterInstance;
|
||||
|
||||
makeObservable(this, {
|
||||
filterConfigs: observable,
|
||||
configOptions: observable,
|
||||
// computed
|
||||
allAvailableConfigs: computed,
|
||||
// helpers
|
||||
register: action,
|
||||
registerAll: action,
|
||||
updateConfigByProperty: action,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------ computed ------------
|
||||
|
||||
/**
|
||||
* Returns all available filterConfigs.
|
||||
* If allowSameFilters is true, all enabled configs are returned.
|
||||
* Otherwise, only configs that are not already applied to the filter instance are returned.
|
||||
* @returns All available filterConfigs.
|
||||
*/
|
||||
get allAvailableConfigs(): IFilterConfigManager<P>["allAvailableConfigs"] {
|
||||
const appliedProperties = new Set(this._filterInstance.allConditions.map((condition) => condition.property));
|
||||
// Return all enabled configs that either allow multiple filters or are not currently applied
|
||||
return this._allEnabledConfigs.filter((config) => config.allowMultipleFilters || !appliedProperties.has(config.id));
|
||||
}
|
||||
|
||||
// ------------ computed functions ------------
|
||||
|
||||
/**
|
||||
* Returns a config by filter property.
|
||||
* @param property - The property to get the config for.
|
||||
* @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>
|
||||
);
|
||||
|
||||
// ------------ helpers ------------
|
||||
|
||||
/**
|
||||
* Register a config.
|
||||
* If a config with the same property already exists, it will be updated with the new values.
|
||||
* Otherwise, a new config will be created.
|
||||
* @param configUpdates - The config updates to register.
|
||||
*/
|
||||
register: IFilterConfigManager<P>["register"] = action((configUpdates) => {
|
||||
if (this.filterConfigs.has(configUpdates.id)) {
|
||||
// Update existing config if it has differences
|
||||
const existingConfig = this.filterConfigs.get(configUpdates.id)!;
|
||||
existingConfig.mutate(configUpdates);
|
||||
} else {
|
||||
// Create new config if it doesn't exist
|
||||
this.filterConfigs.set(configUpdates.id, new FilterConfig(configUpdates));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Register all configs.
|
||||
* @param configs - The configs to register.
|
||||
*/
|
||||
registerAll: IFilterConfigManager<P>["registerAll"] = action((configs) => {
|
||||
configs.forEach((config) => this.register(config));
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates a config by filter property.
|
||||
* @param property - The property of the config to update.
|
||||
* @param configUpdates - The updates to apply to the config.
|
||||
*/
|
||||
updateConfigByProperty: IFilterConfigManager<P>["updateConfigByProperty"] = action((property, configUpdates) => {
|
||||
const prevConfig = this.filterConfigs.get(property);
|
||||
prevConfig?.mutate(configUpdates);
|
||||
});
|
||||
|
||||
// ------------ private computed ------------
|
||||
|
||||
private get _allConfigs(): IFilterConfig<P, TFilterValue>[] {
|
||||
return Array.from(this.filterConfigs.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all enabled filterConfigs.
|
||||
* @returns All enabled filterConfigs.
|
||||
*/
|
||||
private get _allEnabledConfigs(): IFilterConfig<P, TFilterValue>[] {
|
||||
return this._allConfigs.filter((config) => config.isEnabled);
|
||||
}
|
||||
|
||||
// ------------ private helpers ------------
|
||||
|
||||
/**
|
||||
* Initializes the config options.
|
||||
* @param options - The options to initialize the config options with.
|
||||
* @returns The initialized config options.
|
||||
*/
|
||||
private _initializeConfigOptions(options?: Partial<TConfigOptions>): TConfigOptions {
|
||||
return DEFAULT_FILTER_CONFIG_OPTIONS ? { ...DEFAULT_FILTER_CONFIG_OPTIONS, ...options } : options || {};
|
||||
}
|
||||
}
|
||||
212
packages/shared-state/src/store/rich-filters/config.ts
Normal file
212
packages/shared-state/src/store/rich-filters/config.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { EMPTY_OPERATOR_LABEL } from "@plane/constants";
|
||||
import {
|
||||
FILTER_FIELD_TYPE,
|
||||
TSupportedOperators,
|
||||
TFilterConfig,
|
||||
TFilterProperty,
|
||||
TFilterValue,
|
||||
TOperatorSpecificConfigs,
|
||||
TAllAvailableOperatorsForDisplay,
|
||||
} from "@plane/types";
|
||||
import {
|
||||
getOperatorLabel,
|
||||
isDateFilterType,
|
||||
getDateOperatorLabel,
|
||||
isDateFilterOperator,
|
||||
getOperatorForPayload,
|
||||
} from "@plane/utils";
|
||||
|
||||
type TOperatorOptionForDisplay = {
|
||||
value: TAllAvailableOperatorsForDisplay;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface IFilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue>
|
||||
extends TFilterConfig<P, V> {
|
||||
// computed
|
||||
allSupportedOperators: TSupportedOperators[];
|
||||
allSupportedOperatorConfigs: TOperatorSpecificConfigs<V>[keyof TOperatorSpecificConfigs<V>][];
|
||||
firstOperator: TSupportedOperators | undefined;
|
||||
// computed functions
|
||||
getOperatorConfig: (
|
||||
operator: TAllAvailableOperatorsForDisplay
|
||||
) => TOperatorSpecificConfigs<V>[keyof TOperatorSpecificConfigs<V>] | undefined;
|
||||
getLabelForOperator: (operator: TAllAvailableOperatorsForDisplay | undefined) => string;
|
||||
getDisplayOperatorByValue: <T extends TSupportedOperators | TAllAvailableOperatorsForDisplay>(
|
||||
operator: T,
|
||||
value: V
|
||||
) => T;
|
||||
getAllDisplayOperatorOptionsByValue: (value: V) => TOperatorOptionForDisplay[];
|
||||
// actions
|
||||
mutate: (updates: Partial<TFilterConfig<P, V>>) => void;
|
||||
}
|
||||
|
||||
export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue>
|
||||
implements IFilterConfig<P, V>
|
||||
{
|
||||
// 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"];
|
||||
|
||||
/**
|
||||
* Creates a new FilterConfig instance.
|
||||
* @param params - The parameters for the filter config.
|
||||
*/
|
||||
constructor(params: TFilterConfig<P, V>) {
|
||||
this.id = params.id;
|
||||
this.label = params.label;
|
||||
this.icon = params.icon;
|
||||
this.isEnabled = params.isEnabled;
|
||||
this.supportedOperatorConfigsMap = params.supportedOperatorConfigsMap;
|
||||
this.allowMultipleFilters = params.allowMultipleFilters;
|
||||
|
||||
makeObservable(this, {
|
||||
id: observable,
|
||||
label: observable,
|
||||
icon: observable,
|
||||
isEnabled: observable,
|
||||
supportedOperatorConfigsMap: observable,
|
||||
allowMultipleFilters: observable,
|
||||
// computed
|
||||
allSupportedOperators: computed,
|
||||
allSupportedOperatorConfigs: computed,
|
||||
firstOperator: computed,
|
||||
// actions
|
||||
mutate: action,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------ computed ------------
|
||||
|
||||
/**
|
||||
* Returns all supported operators.
|
||||
* @returns All supported operators.
|
||||
*/
|
||||
get allSupportedOperators(): IFilterConfig<P, V>["allSupportedOperators"] {
|
||||
return Array.from(this.supportedOperatorConfigsMap.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all supported operator configs.
|
||||
* @returns All supported operator configs.
|
||||
*/
|
||||
get allSupportedOperatorConfigs(): IFilterConfig<P, V>["allSupportedOperatorConfigs"] {
|
||||
return Array.from(this.supportedOperatorConfigsMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first operator.
|
||||
* @returns The first operator.
|
||||
*/
|
||||
get firstOperator(): IFilterConfig<P, V>["firstOperator"] {
|
||||
return this.allSupportedOperators[0];
|
||||
}
|
||||
|
||||
// ------------ computed functions ------------
|
||||
|
||||
/**
|
||||
* Returns the operator config.
|
||||
* @param operator - The operator.
|
||||
* @returns The operator config.
|
||||
*/
|
||||
getOperatorConfig: IFilterConfig<P, V>["getOperatorConfig"] = computedFn((operator) =>
|
||||
this.supportedOperatorConfigsMap.get(getOperatorForPayload(operator).operator)
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the label for an operator.
|
||||
* @param operator - The operator.
|
||||
* @returns The label for the operator.
|
||||
*/
|
||||
getLabelForOperator: IFilterConfig<P, V>["getLabelForOperator"] = computedFn((operator) => {
|
||||
if (!operator) return EMPTY_OPERATOR_LABEL;
|
||||
|
||||
const operatorConfig = this.getOperatorConfig(operator);
|
||||
|
||||
if (operatorConfig?.operatorLabel) {
|
||||
return operatorConfig.operatorLabel;
|
||||
}
|
||||
|
||||
if (operatorConfig?.type && isDateFilterType(operatorConfig.type) && isDateFilterOperator(operator)) {
|
||||
return getDateOperatorLabel(operator);
|
||||
}
|
||||
|
||||
return getOperatorLabel(operator);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the operator for a value.
|
||||
* @param value - The value.
|
||||
* @returns The operator for the value.
|
||||
*/
|
||||
getDisplayOperatorByValue: IFilterConfig<P, V>["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;
|
||||
}
|
||||
return operator;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns all supported operator options for display in the filter UI.
|
||||
* This method filters out operators that are already applied (unless multiple filters are allowed)
|
||||
* and includes both positive and negative variants when supported.
|
||||
*
|
||||
* @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[] = [];
|
||||
|
||||
// Process each supported operator to build display options
|
||||
for (const operator of this.allSupportedOperators) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return operatorOptions;
|
||||
}
|
||||
);
|
||||
|
||||
// ------------ actions ------------
|
||||
|
||||
/**
|
||||
* Mutates the config.
|
||||
* @param updates - The updates to apply to the config.
|
||||
*/
|
||||
mutate: IFilterConfig<P, V>["mutate"] = action((updates) => {
|
||||
runInAction(() => {
|
||||
for (const key in updates) {
|
||||
if (updates.hasOwnProperty(key)) {
|
||||
const configKey = key as keyof TFilterConfig<P, V>;
|
||||
set(this, configKey, updates[configKey]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ------------ private helpers ------------
|
||||
|
||||
private _getAdditionalOperatorOptions = (
|
||||
_operator: TSupportedOperators,
|
||||
_value: V
|
||||
): TOperatorOptionForDisplay | undefined => undefined;
|
||||
}
|
||||
172
packages/shared-state/src/store/rich-filters/filter-helpers.ts
Normal file
172
packages/shared-state/src/store/rich-filters/filter-helpers.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { toJS } from "mobx";
|
||||
// plane imports
|
||||
import { DEFAULT_FILTER_EXPRESSION_OPTIONS, TExpressionOptions } from "@plane/constants";
|
||||
import {
|
||||
IFilterAdapter,
|
||||
LOGICAL_OPERATOR,
|
||||
TSupportedOperators,
|
||||
TFilterExpression,
|
||||
TFilterValue,
|
||||
TFilterProperty,
|
||||
TExternalFilter,
|
||||
TLogicalOperator,
|
||||
TFilterConditionPayload,
|
||||
} from "@plane/types";
|
||||
import { addAndCondition, createConditionNode, updateNodeInExpression } from "@plane/utils";
|
||||
|
||||
/**
|
||||
* Interface for filter instance helper utilities.
|
||||
* Provides comprehensive methods for filter expression manipulation, node operations,
|
||||
* operator utilities, and expression restructuring.
|
||||
* @template P - The filter property type extending TFilterProperty
|
||||
* @template E - The external filter type extending TExternalFilter
|
||||
*/
|
||||
export interface IFilterInstanceHelper<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
// initialization
|
||||
initializeExpression: (initialExpression?: E) => TFilterExpression<P> | null;
|
||||
initializeExpressionOptions: (expressionOptions?: Partial<TExpressionOptions<E>>) => TExpressionOptions<E>;
|
||||
// condition operations
|
||||
addConditionToExpression: <V extends TFilterValue>(
|
||||
expression: TFilterExpression<P> | null,
|
||||
groupOperator: TLogicalOperator,
|
||||
condition: TFilterConditionPayload<P, V>,
|
||||
isNegation: boolean
|
||||
) => TFilterExpression<P> | null;
|
||||
// group operations
|
||||
restructureExpressionForOperatorChange: (
|
||||
expression: TFilterExpression<P> | null,
|
||||
conditionId: string,
|
||||
newOperator: TSupportedOperators,
|
||||
isNegation: boolean,
|
||||
shouldResetValue: boolean
|
||||
) => TFilterExpression<P> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive helper class for filter instance operations.
|
||||
* Provides utilities for filter expression manipulation, node operations,
|
||||
* operator transformations, and expression restructuring.
|
||||
*
|
||||
* @template K - The filter property type extending TFilterProperty
|
||||
* @template E - The external filter type extending TExternalFilter
|
||||
*/
|
||||
export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternalFilter>
|
||||
implements IFilterInstanceHelper<P, E>
|
||||
{
|
||||
private adapter: IFilterAdapter<P, E>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// ------------ initialization ------------
|
||||
|
||||
/**
|
||||
* Initializes the filter expression from external format.
|
||||
* @param initialExpression - The initial expression to initialize the filter with
|
||||
* @returns The initialized filter expression or null if no initial expression provided
|
||||
*/
|
||||
initializeExpression: IFilterInstanceHelper<P, E>["initializeExpression"] = (initialExpression) => {
|
||||
if (!initialExpression) return null;
|
||||
return this.adapter.toInternal(toJS(cloneDeep(initialExpression)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the filter expression options with defaults.
|
||||
* @param expressionOptions - Optional expression options to override defaults
|
||||
* @returns The initialized filter expression options
|
||||
*/
|
||||
initializeExpressionOptions: IFilterInstanceHelper<P, E>["initializeExpressionOptions"] = (expressionOptions) => ({
|
||||
...DEFAULT_FILTER_EXPRESSION_OPTIONS,
|
||||
...expressionOptions,
|
||||
});
|
||||
|
||||
// ------------ condition operations ------------
|
||||
|
||||
/**
|
||||
* Adds a condition to the filter expression based on the logical operator.
|
||||
* @param expression - The current filter expression
|
||||
* @param groupOperator - The logical operator to use for the condition
|
||||
* @param condition - The condition to add
|
||||
* @param isNegation - Whether the condition should be negated
|
||||
* @returns The updated filter expression
|
||||
*/
|
||||
addConditionToExpression: IFilterInstanceHelper<P, E>["addConditionToExpression"] = (
|
||||
expression,
|
||||
groupOperator,
|
||||
condition,
|
||||
isNegation
|
||||
) => this._addConditionByOperator(expression, groupOperator, this._getConditionPayloadToAdd(condition, isNegation));
|
||||
|
||||
// ------------ group operations ------------
|
||||
|
||||
/**
|
||||
* Restructures the expression when a condition's operator changes between positive and negative.
|
||||
* @param expression - The filter expression to operate on
|
||||
* @param conditionId - The ID of the condition being updated
|
||||
* @param newOperator - The new operator for the condition
|
||||
* @param isNegation - Whether the operator is negation
|
||||
* @param shouldResetValue - Whether to reset the condition value
|
||||
* @returns The restructured expression
|
||||
*/
|
||||
restructureExpressionForOperatorChange: IFilterInstanceHelper<P, E>["restructureExpressionForOperatorChange"] = (
|
||||
expression,
|
||||
conditionId,
|
||||
newOperator,
|
||||
_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;
|
||||
};
|
||||
|
||||
// ------------ private helpers ------------
|
||||
|
||||
/**
|
||||
* Gets the condition payload to add to the expression.
|
||||
* @param conditionNode - The condition node to add
|
||||
* @param isNegation - Whether the condition should be negated
|
||||
* @returns The condition payload to add
|
||||
*/
|
||||
private _getConditionPayloadToAdd = (
|
||||
condition: TFilterConditionPayload<P, TFilterValue>,
|
||||
_isNegation: boolean
|
||||
): TFilterExpression<P> => {
|
||||
const conditionNode = createConditionNode(condition);
|
||||
|
||||
return conditionNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the logical operator switch for adding conditions.
|
||||
* @param expression - The current expression
|
||||
* @param groupOperator - The logical operator
|
||||
* @param conditionToAdd - The condition to add
|
||||
* @returns The updated expression
|
||||
*/
|
||||
private _addConditionByOperator(
|
||||
expression: TFilterExpression<P> | null,
|
||||
groupOperator: TLogicalOperator,
|
||||
conditionToAdd: TFilterExpression<P>
|
||||
): TFilterExpression<P> | null {
|
||||
switch (groupOperator) {
|
||||
case LOGICAL_OPERATOR.AND:
|
||||
return addAndCondition(expression, conditionToAdd);
|
||||
default:
|
||||
console.warn(`Unsupported logical operator: ${groupOperator}`);
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
490
packages/shared-state/src/store/rich-filters/filter.ts
Normal file
490
packages/shared-state/src/store/rich-filters/filter.ts
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { action, computed, makeObservable, observable, toJS } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import {
|
||||
TClearFilterOptions,
|
||||
TExpressionOptions,
|
||||
TFilterOptions,
|
||||
TSaveViewOptions,
|
||||
TUpdateViewOptions,
|
||||
} from "@plane/constants";
|
||||
import {
|
||||
FILTER_NODE_TYPE,
|
||||
IFilterAdapter,
|
||||
SingleOrArray,
|
||||
TAllAvailableOperatorsForDisplay,
|
||||
TExternalFilter,
|
||||
TFilterConditionNode,
|
||||
TFilterConditionNodeForDisplay,
|
||||
TFilterConditionPayload,
|
||||
TFilterExpression,
|
||||
TFilterProperty,
|
||||
TFilterValue,
|
||||
TLogicalOperator,
|
||||
TSupportedOperators,
|
||||
} from "@plane/types";
|
||||
// local imports
|
||||
import {
|
||||
deepCompareFilterExpressions,
|
||||
extractConditions,
|
||||
extractConditionsWithDisplayOperators,
|
||||
findConditionsByPropertyAndOperator,
|
||||
findNodeById,
|
||||
hasValidValue,
|
||||
removeNodeFromExpression,
|
||||
sanitizeAndStabilizeExpression,
|
||||
shouldNotifyChangeForExpression,
|
||||
updateNodeInExpression,
|
||||
} from "@plane/utils";
|
||||
import { FilterConfigManager, IFilterConfigManager } from "./config-manager";
|
||||
import { FilterInstanceHelper, IFilterInstanceHelper } from "./filter-helpers";
|
||||
|
||||
/**
|
||||
* Interface for a filter instance.
|
||||
* Provides methods to manage the filter expression and notify changes.
|
||||
* - id: The id of the filter instance
|
||||
* - expression: The filter expression
|
||||
* - adapter: The filter adapter
|
||||
* - configManager: The filter config manager
|
||||
* - onExpressionChange: The callback to notify when the expression changes
|
||||
* - hasActiveFilters: Whether the filter instance has any active filters
|
||||
* - allConditions: All conditions in the filter expression
|
||||
* - allConditionsForDisplay: All conditions in the filter expression
|
||||
* - addCondition: Adds a condition to the filter expression
|
||||
* - updateConditionOperator: Updates the operator of a condition in the filter expression
|
||||
* - updateConditionValue: Updates the value of a condition in the filter expression
|
||||
* - removeCondition: Removes a condition from the filter expression
|
||||
* - clearFilters: Clears the filter expression
|
||||
* @template P - The filter property type extending TFilterProperty
|
||||
* @template E - The external filter type extending TExternalFilter
|
||||
*/
|
||||
export interface IFilterInstance<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
// observables
|
||||
id: string;
|
||||
initialFilterExpression: TFilterExpression<P> | null;
|
||||
expression: TFilterExpression<P> | null;
|
||||
adapter: IFilterAdapter<P, E>;
|
||||
configManager: IFilterConfigManager<P>;
|
||||
onExpressionChange?: (expression: E) => void;
|
||||
// computed
|
||||
hasActiveFilters: boolean;
|
||||
hasChanges: boolean;
|
||||
allConditions: TFilterConditionNode<P, TFilterValue>[];
|
||||
allConditionsForDisplay: TFilterConditionNodeForDisplay<P, TFilterValue>[];
|
||||
// computed option helpers
|
||||
clearFilterOptions: TClearFilterOptions | undefined;
|
||||
saveViewOptions: TSaveViewOptions<E> | undefined;
|
||||
updateViewOptions: TUpdateViewOptions<E> | undefined;
|
||||
// computed permissions
|
||||
canClearFilters: boolean;
|
||||
canSaveView: boolean;
|
||||
canUpdateView: boolean;
|
||||
// filter expression actions
|
||||
resetExpression: (externalExpression: E, shouldResetInitialExpression?: boolean) => void;
|
||||
// filter condition
|
||||
findConditionsByPropertyAndOperator: (
|
||||
property: P,
|
||||
operator: TAllAvailableOperatorsForDisplay
|
||||
) => TFilterConditionNodeForDisplay<P, TFilterValue>[];
|
||||
findFirstConditionByPropertyAndOperator: (
|
||||
property: P,
|
||||
operator: TAllAvailableOperatorsForDisplay
|
||||
) => TFilterConditionNodeForDisplay<P, TFilterValue> | undefined;
|
||||
addCondition: <V extends TFilterValue>(
|
||||
groupOperator: TLogicalOperator,
|
||||
condition: TFilterConditionPayload<P, V>,
|
||||
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;
|
||||
// config actions
|
||||
clearFilters: () => Promise<void>;
|
||||
saveView: () => Promise<void>;
|
||||
updateView: () => Promise<void>;
|
||||
// expression options actions
|
||||
updateExpressionOptions: (newOptions: Partial<TExpressionOptions<E>>) => void;
|
||||
}
|
||||
|
||||
export type TFilterParams<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
adapter: IFilterAdapter<P, E>;
|
||||
options?: Partial<TFilterOptions<E>>;
|
||||
initialExpression?: E;
|
||||
onExpressionChange?: (expression: E) => void;
|
||||
};
|
||||
|
||||
export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter> implements IFilterInstance<P, E> {
|
||||
// observables
|
||||
id: string;
|
||||
initialFilterExpression: TFilterExpression<P> | null;
|
||||
expression: TFilterExpression<P> | null;
|
||||
expressionOptions: TExpressionOptions<E>;
|
||||
adapter: IFilterAdapter<P, E>;
|
||||
configManager: IFilterConfigManager<P>;
|
||||
onExpressionChange?: (expression: E) => void;
|
||||
|
||||
// helper instance
|
||||
private helper: IFilterInstanceHelper<P, E>;
|
||||
|
||||
constructor(params: TFilterParams<P, E>) {
|
||||
this.id = uuidv4();
|
||||
this.adapter = params.adapter;
|
||||
this.helper = new FilterInstanceHelper<P, E>(this.adapter);
|
||||
this.configManager = new FilterConfigManager<P, E>(this, {
|
||||
options: params.options?.config,
|
||||
});
|
||||
// initialize expression
|
||||
const initialExpression = this.helper.initializeExpression(params.initialExpression);
|
||||
this.initialFilterExpression = cloneDeep(initialExpression);
|
||||
this.expression = cloneDeep(initialExpression);
|
||||
this.expressionOptions = this.helper.initializeExpressionOptions(params.options?.expression);
|
||||
this.onExpressionChange = params.onExpressionChange;
|
||||
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
id: observable,
|
||||
initialFilterExpression: observable,
|
||||
expression: observable,
|
||||
expressionOptions: observable,
|
||||
adapter: observable,
|
||||
configManager: observable,
|
||||
// computed
|
||||
hasActiveFilters: computed,
|
||||
hasChanges: computed,
|
||||
allConditions: computed,
|
||||
allConditionsForDisplay: computed,
|
||||
// computed option helpers
|
||||
clearFilterOptions: computed,
|
||||
saveViewOptions: computed,
|
||||
updateViewOptions: computed,
|
||||
// computed permissions
|
||||
canClearFilters: computed,
|
||||
canSaveView: computed,
|
||||
canUpdateView: computed,
|
||||
// actions
|
||||
resetExpression: action,
|
||||
findConditionsByPropertyAndOperator: action,
|
||||
findFirstConditionByPropertyAndOperator: action,
|
||||
addCondition: action,
|
||||
updateConditionOperator: action,
|
||||
updateConditionValue: action,
|
||||
removeCondition: action,
|
||||
clearFilters: action,
|
||||
saveView: action,
|
||||
updateView: action,
|
||||
updateExpressionOptions: action,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------ computed ------------
|
||||
|
||||
/**
|
||||
* Checks if the filter instance has any active filters.
|
||||
* @returns True if the filter instance has any active filters, false otherwise.
|
||||
*/
|
||||
get hasActiveFilters(): IFilterInstance<P, E>["hasActiveFilters"] {
|
||||
// if the expression is null, return false
|
||||
if (!this.expression) return false;
|
||||
// if there are no conditions, return false
|
||||
if (this.allConditionsForDisplay.length === 0) return false;
|
||||
// if there are conditions, return true if any of them have a valid value
|
||||
return this.allConditionsForDisplay.some((condition) => hasValidValue(condition.value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filter instance has any changes with respect to the initial expression.
|
||||
* @returns True if the filter instance has any changes, false otherwise.
|
||||
*/
|
||||
get hasChanges(): IFilterInstance<P, E>["hasChanges"] {
|
||||
return !deepCompareFilterExpressions(this.initialFilterExpression, this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all conditions from the filter expression.
|
||||
* @returns An array of filter conditions.
|
||||
*/
|
||||
get allConditions(): IFilterInstance<P, E>["allConditions"] {
|
||||
if (!this.expression) return [];
|
||||
return extractConditions(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all conditions in the filter expression for display purposes.
|
||||
* @returns An array of filter conditions for display purposes.
|
||||
*/
|
||||
get allConditionsForDisplay(): IFilterInstance<P, E>["allConditionsForDisplay"] {
|
||||
if (!this.expression) return [];
|
||||
return extractConditionsWithDisplayOperators(this.expression);
|
||||
}
|
||||
|
||||
// ------------ computed option helpers ------------
|
||||
|
||||
/**
|
||||
* Returns the clear filter options.
|
||||
* @returns The clear filter options.
|
||||
*/
|
||||
get clearFilterOptions(): IFilterInstance<P, E>["clearFilterOptions"] {
|
||||
return this.expressionOptions.clearFilterOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the save view options.
|
||||
* @returns The save view options.
|
||||
*/
|
||||
get saveViewOptions(): IFilterInstance<P, E>["saveViewOptions"] {
|
||||
return this.expressionOptions.saveViewOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the update view options.
|
||||
* @returns The update view options.
|
||||
*/
|
||||
get updateViewOptions(): IFilterInstance<P, E>["updateViewOptions"] {
|
||||
return this.expressionOptions.updateViewOptions;
|
||||
}
|
||||
|
||||
// ------------ computed permissions ------------
|
||||
|
||||
/**
|
||||
* Checks if the filter expression can be cleared.
|
||||
* @returns True if the filter expression can be cleared, false otherwise.
|
||||
*/
|
||||
get canClearFilters(): IFilterInstance<P, E>["canClearFilters"] {
|
||||
if (!this.expression) return false;
|
||||
if (this.allConditionsForDisplay.length === 0) return false;
|
||||
return this.clearFilterOptions ? !this.clearFilterOptions.isDisabled : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filter expression can be saved as a view.
|
||||
* @returns True if the filter instance can be saved, false otherwise.
|
||||
*/
|
||||
get canSaveView(): IFilterInstance<P, E>["canSaveView"] {
|
||||
return this.hasActiveFilters && !!this.saveViewOptions && !this.saveViewOptions.isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filter expression can be updated as a view.
|
||||
* @returns True if the filter expression can be updated, false otherwise.
|
||||
*/
|
||||
get canUpdateView(): IFilterInstance<P, E>["canUpdateView"] {
|
||||
return (
|
||||
!!this.updateViewOptions &&
|
||||
(this.hasChanges || !!this.updateViewOptions.hasAdditionalChanges) &&
|
||||
!this.updateViewOptions.isDisabled
|
||||
);
|
||||
}
|
||||
|
||||
// ------------ actions ------------
|
||||
|
||||
/**
|
||||
* Resets the filter expression to the initial expression.
|
||||
* @param externalExpression - The external expression to reset to.
|
||||
*/
|
||||
resetExpression: IFilterInstance<P, E>["resetExpression"] = action(
|
||||
(externalExpression, shouldResetInitialExpression = true) => {
|
||||
this.expression = this.helper.initializeExpression(externalExpression);
|
||||
if (shouldResetInitialExpression) {
|
||||
this._resetInitialFilterExpression();
|
||||
}
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds all conditions by property and operator.
|
||||
* @param property - The property to find the conditions by.
|
||||
* @param operator - The operator to find the conditions by.
|
||||
* @returns All the conditions that match the property and operator.
|
||||
*/
|
||||
findConditionsByPropertyAndOperator: IFilterInstance<P, E>["findConditionsByPropertyAndOperator"] = action(
|
||||
(property, operator) => {
|
||||
if (!this.expression) return [];
|
||||
return findConditionsByPropertyAndOperator(this.expression, property, operator);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first condition by property and operator.
|
||||
* @param property - The property to find the condition by.
|
||||
* @param operator - The operator to find the condition by.
|
||||
* @returns The first condition that matches the property and operator.
|
||||
*/
|
||||
findFirstConditionByPropertyAndOperator: IFilterInstance<P, E>["findFirstConditionByPropertyAndOperator"] = action(
|
||||
(property, operator) => {
|
||||
if (!this.expression) return undefined;
|
||||
const conditions = findConditionsByPropertyAndOperator(this.expression, property, operator);
|
||||
return conditions[0];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds a condition to the filter expression.
|
||||
* @param groupOperator - The logical operator to use for the condition.
|
||||
* @param condition - The condition to add.
|
||||
* @param isNegation - Whether the condition should be negated.
|
||||
*/
|
||||
addCondition: IFilterInstance<P, E>["addCondition"] = action((groupOperator, condition, isNegation = false) => {
|
||||
const conditionValue = condition.value;
|
||||
|
||||
this.expression = this.helper.addConditionToExpression(this.expression, groupOperator, condition, isNegation);
|
||||
|
||||
if (hasValidValue(conditionValue)) {
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the operator of a condition in the filter expression.
|
||||
* @param conditionId - The id of the condition to update.
|
||||
* @param operator - The new operator for the condition.
|
||||
*/
|
||||
updateConditionOperator: IFilterInstance<P, E>["updateConditionOperator"] = action(
|
||||
(conditionId: string, operator: TSupportedOperators, isNegation: boolean) => {
|
||||
if (!this.expression) return;
|
||||
const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
|
||||
if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;
|
||||
|
||||
// Get the operator configs for the current and new operators
|
||||
const currentOperatorConfig = this.configManager
|
||||
.getConfigByProperty(conditionBeforeUpdate.property)
|
||||
?.getOperatorConfig(conditionBeforeUpdate.operator);
|
||||
const newOperatorConfig = this.configManager
|
||||
.getConfigByProperty(conditionBeforeUpdate.property)
|
||||
?.getOperatorConfig(operator);
|
||||
// Reset the value if the operator config types are different
|
||||
const shouldResetConditionValue = currentOperatorConfig?.type !== newOperatorConfig?.type;
|
||||
|
||||
// Use restructuring logic for operator changes
|
||||
const updatedExpression = this.helper.restructureExpressionForOperatorChange(
|
||||
this.expression,
|
||||
conditionId,
|
||||
operator,
|
||||
isNegation,
|
||||
shouldResetConditionValue
|
||||
);
|
||||
|
||||
if (updatedExpression) {
|
||||
this.expression = updatedExpression;
|
||||
}
|
||||
|
||||
if (hasValidValue(conditionBeforeUpdate.value)) {
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
updateConditionValue: IFilterInstance<P, E>["updateConditionValue"] = action(
|
||||
<V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => {
|
||||
// If the expression is not valid, return
|
||||
if (!this.expression) return;
|
||||
|
||||
// If the value is not valid, remove the condition
|
||||
if (!hasValidValue(value)) {
|
||||
this.removeCondition(conditionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the condition value
|
||||
updateNodeInExpression(this.expression, conditionId, {
|
||||
value,
|
||||
});
|
||||
|
||||
// Notify the change
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes a condition from the filter expression.
|
||||
* @param conditionId - The id of the condition to remove.
|
||||
*/
|
||||
removeCondition: IFilterInstance<P, E>["removeCondition"] = action((conditionId) => {
|
||||
if (!this.expression) return;
|
||||
const { expression, shouldNotify } = removeNodeFromExpression(this.expression, conditionId);
|
||||
this.expression = expression;
|
||||
if (shouldNotify) {
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the filter expression.
|
||||
*/
|
||||
clearFilters: IFilterInstance<P, E>["clearFilters"] = action(async () => {
|
||||
if (this.canClearFilters) {
|
||||
const shouldNotify = shouldNotifyChangeForExpression(this.expression);
|
||||
this.expression = null;
|
||||
await this.clearFilterOptions?.onFilterClear();
|
||||
if (shouldNotify) {
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
} else {
|
||||
console.warn("Cannot clear filters: invalid expression or missing options.");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Saves the filter expression.
|
||||
*/
|
||||
saveView: IFilterInstance<P, E>["saveView"] = action(async () => {
|
||||
if (this.canSaveView && this.saveViewOptions) {
|
||||
await this.saveViewOptions.onViewSave(this._getExternalExpression());
|
||||
} else {
|
||||
console.warn("Cannot save view: invalid expression or missing options.");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the filter expression.
|
||||
*/
|
||||
updateView: IFilterInstance<P, E>["updateView"] = action(async () => {
|
||||
if (this.canUpdateView && this.updateViewOptions) {
|
||||
await this.updateViewOptions.onViewUpdate(this._getExternalExpression());
|
||||
this._resetInitialFilterExpression();
|
||||
} else {
|
||||
console.warn("Cannot update view: invalid expression or missing options.");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the expression options for the filter instance.
|
||||
* This allows dynamic updates to options like isDisabled properties.
|
||||
*/
|
||||
updateExpressionOptions: IFilterInstance<P, E>["updateExpressionOptions"] = action((newOptions) => {
|
||||
this.expressionOptions = {
|
||||
...this.expressionOptions,
|
||||
...newOptions,
|
||||
};
|
||||
});
|
||||
|
||||
// ------------ private helpers ------------
|
||||
/**
|
||||
* Resets the initial filter expression to the current expression.
|
||||
*/
|
||||
private _resetInitialFilterExpression(): void {
|
||||
this.initialFilterExpression = cloneDeep(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external filter representation of the filter instance.
|
||||
* @returns The external filter representation of the filter instance.
|
||||
*/
|
||||
private _getExternalExpression = computedFn(() =>
|
||||
this.adapter.toExternal(sanitizeAndStabilizeExpression(toJS(this.expression)))
|
||||
);
|
||||
|
||||
/**
|
||||
* Notifies the parent component of the expression change.
|
||||
*/
|
||||
private _notifyExpressionChange(): void {
|
||||
this.onExpressionChange?.(this._getExternalExpression());
|
||||
}
|
||||
}
|
||||
2
packages/shared-state/src/store/rich-filters/index.ts
Normal file
2
packages/shared-state/src/store/rich-filters/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./adapter";
|
||||
export * from "./filter";
|
||||
1
packages/shared-state/src/utils/index.ts
Normal file
1
packages/shared-state/src/utils/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./rich-filter.helper";
|
||||
47
packages/shared-state/src/utils/rich-filter.helper.ts
Normal file
47
packages/shared-state/src/utils/rich-filter.helper.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// plane imports
|
||||
import {
|
||||
LOGICAL_OPERATOR,
|
||||
TBuildFilterExpressionParams,
|
||||
TExternalFilter,
|
||||
TFilterProperty,
|
||||
TFilterValue,
|
||||
} from "@plane/types";
|
||||
import { getOperatorForPayload } from "@plane/utils";
|
||||
// local imports
|
||||
import { FilterInstance } from "../store/rich-filters/filter";
|
||||
|
||||
/**
|
||||
* Builds a temporary filter expression from conditions.
|
||||
* @param params.conditions - The conditions for building the filter expression.
|
||||
* @param params.adapter - The adapter for building the filter expression.
|
||||
* @returns The temporary filter expression.
|
||||
*/
|
||||
export const buildTempFilterExpressionFromConditions = <
|
||||
P extends TFilterProperty,
|
||||
V extends TFilterValue,
|
||||
E extends TExternalFilter,
|
||||
>(
|
||||
params: TBuildFilterExpressionParams<P, V, E>
|
||||
): E | undefined => {
|
||||
const { conditions, adapter } = params;
|
||||
let tempExpression: E | undefined = undefined;
|
||||
const tempFilterInstance = new FilterInstance<P, E>({
|
||||
adapter,
|
||||
onExpressionChange: (expression) => {
|
||||
tempExpression = expression;
|
||||
},
|
||||
});
|
||||
for (const condition of conditions) {
|
||||
const { operator, isNegation } = getOperatorForPayload(condition.operator);
|
||||
tempFilterInstance.addCondition(
|
||||
LOGICAL_OPERATOR.AND,
|
||||
{
|
||||
property: condition.property,
|
||||
operator,
|
||||
value: condition.value,
|
||||
},
|
||||
isNegation
|
||||
);
|
||||
}
|
||||
return tempExpression;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue