Fix filters + add behaviour for select (#13997)

On field selection, we need to retrieve the fieldMetadata to set the
filter type. Current code was using the current step filter
fieldMetdataId instead of the new one.

Also adding a special behaviour for Select filter: it behaves as
arrays/multiselect but the opearands are not the same.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette
2025-08-20 17:14:31 +02:00
committed by GitHub
parent f9abea2387
commit 4b66e8021a
17 changed files with 287 additions and 146 deletions
@@ -314,6 +314,80 @@ describe('evaluateFilterConditions', () => {
});
});
describe('Select filter operands', () => {
it('should return true when there are common values (SELECT)', () => {
const filter = createFilter(
ViewFilterOperand.Is,
['John'],
['John', 'Jane'],
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should return false when there are no common values (SELECT)', () => {
const filter = createFilter(
ViewFilterOperand.Is,
['John'],
['Jane'],
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false);
});
it('should return true when there are no common values (IsNot)', () => {
const filter = createFilter(
ViewFilterOperand.IsNot,
['John'],
['Jane'],
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should return true when there are no values (IsEmpty)', () => {
const filter = createFilter(
ViewFilterOperand.IsEmpty,
[],
'',
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should return false when there is a value (IsEmpty)', () => {
const filter = createFilter(
ViewFilterOperand.IsEmpty,
['John'],
'',
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false);
});
it('should return true when there are values (IsNotEmpty)', () => {
const filter = createFilter(
ViewFilterOperand.IsNotEmpty,
['John'],
'',
'SELECT',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
});
describe('Boolean filter operands', () => {
it('should return true when boolean values are equal', () => {
const filter = createFilter(
@@ -1,4 +1,9 @@
import { isObject, isString } from '@sniptt/guards';
import {
isNonEmptyArray,
isNonEmptyString,
isObject,
isString,
} from '@sniptt/guards';
import {
type StepFilter,
type StepFilterGroup,
@@ -21,7 +26,6 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
case 'DATE_TIME':
return evaluateDateFilter(filter);
case 'TEXT':
case 'SELECT':
case 'MULTI_SELECT':
case 'FULL_NAME':
case 'EMAILS':
@@ -31,6 +35,8 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
case 'ARRAY':
case 'RAW_JSON':
return evaluateTextAndArrayFilter(filter);
case 'SELECT':
return evaluateSelectFilter(filter);
case 'BOOLEAN':
return evaluateBooleanFilter(filter);
case 'UUID':
@@ -121,20 +127,11 @@ function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean {
case ViewFilterOperand.DoesNotContain:
return !contains(filter.leftOperand, filter.rightOperand);
case ViewFilterOperand.IsEmpty:
return (
filter.leftOperand === null ||
filter.leftOperand === undefined ||
filter.leftOperand === '' ||
(Array.isArray(filter.leftOperand) && filter.leftOperand.length === 0)
);
return !isNotEmptyTextOrArray(filter.leftOperand);
case ViewFilterOperand.IsNotEmpty:
return (
filter.leftOperand !== null &&
filter.leftOperand !== undefined &&
filter.leftOperand !== '' &&
(!Array.isArray(filter.leftOperand) || filter.leftOperand.length > 0)
);
return isNotEmptyTextOrArray(filter.leftOperand);
default:
throw new Error(
`Operand ${filter.operand} not supported for this filter type`,
@@ -142,6 +139,10 @@ function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean {
}
}
function isNotEmptyTextOrArray(value: unknown): boolean {
return isNonEmptyString(value) || isNonEmptyArray(value);
}
function evaluateBooleanFilter(filter: ResolvedFilter): boolean {
switch (filter.operand) {
case ViewFilterOperand.Is:
@@ -255,17 +256,9 @@ function evaluateCurrencyFilter(filter: ResolvedFilter): boolean {
case ViewFilterOperand.IsNot:
return filter.leftOperand !== filter.rightOperand;
case ViewFilterOperand.IsEmpty:
return (
filter.leftOperand === null ||
filter.leftOperand === undefined ||
filter.leftOperand === ''
);
return !isNonEmptyString(filter.leftOperand);
case ViewFilterOperand.IsNotEmpty:
return (
filter.leftOperand !== null &&
filter.leftOperand !== undefined &&
filter.leftOperand !== ''
);
return isNonEmptyString(filter.leftOperand);
default:
throw new Error(
`Operand ${filter.operand} not supported for currency filter`,
@@ -288,10 +281,10 @@ function evaluateNumberFilter(filter: ResolvedFilter): boolean {
return Number(leftValue) <= Number(rightValue);
case ViewFilterOperand.IsEmpty:
return leftValue === null || leftValue === undefined || leftValue === '';
return !isNonEmptyString(leftValue);
case ViewFilterOperand.IsNotEmpty:
return leftValue !== null && leftValue !== undefined && leftValue !== '';
return isNonEmptyString(leftValue);
default:
throw new Error(
@@ -310,19 +303,9 @@ function evaluateDefaultFilter(filter: ResolvedFilter): boolean {
case ViewFilterOperand.IsNot:
return leftValue != rightValue;
case ViewFilterOperand.IsEmpty:
return (
leftValue === null ||
leftValue === undefined ||
leftValue === '' ||
(Array.isArray(leftValue) && leftValue.length === 0)
);
return !isNotEmptyTextOrArray(leftValue);
case ViewFilterOperand.IsNotEmpty:
return (
leftValue !== null &&
leftValue !== undefined &&
leftValue !== '' &&
(!Array.isArray(leftValue) || leftValue.length > 0)
);
return isNotEmptyTextOrArray(leftValue);
case ViewFilterOperand.Contains:
return contains(leftValue, rightValue);
case ViewFilterOperand.DoesNotContain:
@@ -338,6 +321,24 @@ function evaluateDefaultFilter(filter: ResolvedFilter): boolean {
}
}
function evaluateSelectFilter(filter: ResolvedFilter): boolean {
switch (filter.operand) {
case ViewFilterOperand.Is:
return contains(filter.leftOperand, filter.rightOperand);
case ViewFilterOperand.IsNot:
return !contains(filter.leftOperand, filter.rightOperand);
case ViewFilterOperand.IsEmpty:
return !isNotEmptyTextOrArray(filter.leftOperand);
case ViewFilterOperand.IsNotEmpty:
return isNotEmptyTextOrArray(filter.leftOperand);
default:
throw new Error(
`Operand ${filter.operand} not supported for select filter`,
);
}
}
export function evaluateFilterConditions({
filterGroups = [],
filters = [],