Refacto filter action + handle missing composite fields (#13557)
- handle missing composite fields - refacto filter action to be more reliable Remaining improvements: - filter out some field types - improve relations
This commit is contained in:
+339
-56
@@ -17,9 +17,10 @@ describe('evaluateFilterConditions', () => {
|
||||
operand: ViewFilterOperand,
|
||||
leftOperand: unknown,
|
||||
rightOperand: unknown,
|
||||
type = 'TEXT',
|
||||
): ResolvedFilter => ({
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: type,
|
||||
label: 'Test Filter',
|
||||
rightOperand,
|
||||
operand,
|
||||
@@ -46,46 +47,83 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('single filter operands', () => {
|
||||
describe('Is operand', () => {
|
||||
it('should return true when values are equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, 'John', 'John');
|
||||
describe('Relation/UUID filter operands', () => {
|
||||
it('should return true when values are equal (RELATION)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
'John',
|
||||
'John',
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when values are loosely equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, '123', 123);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when values are not equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, 'John', 'Jane');
|
||||
it('should return false when values are not equal (RELATION)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
'John',
|
||||
'Jane',
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are equal but different types', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, '123', 123);
|
||||
it('should return false when values are equal (IsNot RELATION)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
'John',
|
||||
'John',
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are not equal (IsNot RELATION)', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsNot,
|
||||
'John',
|
||||
'Jane',
|
||||
'RELATION',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IsNot operand', () => {
|
||||
it('should return false when values are equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.IsNot, 'John', 'John');
|
||||
describe('Boolean filter operands', () => {
|
||||
it('should return true when boolean values are equal', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
true,
|
||||
true,
|
||||
'BOOLEAN',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when boolean values are not equal', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
true,
|
||||
false,
|
||||
'BOOLEAN',
|
||||
);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are not equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.IsNot, 'John', 'Jane');
|
||||
it('should handle truthy/falsy conversion', () => {
|
||||
const filter = createFilter(ViewFilterOperand.Is, 1, true, 'BOOLEAN');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
@@ -98,16 +136,19 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
25,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
30,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
20,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
@@ -116,9 +157,24 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle LessThanOrEqual operand correctly', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.LessThanOrEqual, 25, 25);
|
||||
const filter2 = createFilter(ViewFilterOperand.LessThanOrEqual, 20, 25);
|
||||
const filter3 = createFilter(ViewFilterOperand.LessThanOrEqual, 30, 25);
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
25,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
20,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
30,
|
||||
25,
|
||||
'NUMBER',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@@ -132,11 +188,13 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.Contains,
|
||||
'Hello World',
|
||||
'World',
|
||||
'TEXT',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
'Hello',
|
||||
'World',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
@@ -148,11 +206,13 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
'Hello World',
|
||||
'World',
|
||||
'TEXT',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
'Hello',
|
||||
'World',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
@@ -164,11 +224,13 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.Contains,
|
||||
['apple', 'banana', 'cherry'],
|
||||
['apple'],
|
||||
'ARRAY',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
['apple', 'banana', 'cherry'],
|
||||
['grape'],
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
@@ -180,11 +242,13 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
['apple', 'banana', 'cherry'],
|
||||
['apple'],
|
||||
'ARRAY',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
['apple', 'banana', 'cherry'],
|
||||
['grape'],
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
@@ -194,14 +258,30 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('empty operands', () => {
|
||||
it('should handle IsEmpty operand correctly', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.IsEmpty, null, '');
|
||||
const filter2 = createFilter(ViewFilterOperand.IsEmpty, undefined, '');
|
||||
const filter3 = createFilter(ViewFilterOperand.IsEmpty, '', '');
|
||||
const filter4 = createFilter(ViewFilterOperand.IsEmpty, [], '');
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
null,
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
undefined,
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
const filter3 = createFilter(ViewFilterOperand.IsEmpty, '', '', 'TEXT');
|
||||
const filter4 = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
[],
|
||||
'',
|
||||
'ARRAY',
|
||||
);
|
||||
const filter5 = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
'not empty',
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
@@ -216,15 +296,32 @@ describe('evaluateFilterConditions', () => {
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
'not empty',
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
['item'],
|
||||
'',
|
||||
'ARRAY',
|
||||
);
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
null,
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
const filter4 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
'',
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
const filter5 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
[],
|
||||
'',
|
||||
'ARRAY',
|
||||
);
|
||||
const filter3 = createFilter(ViewFilterOperand.IsNotEmpty, null, '');
|
||||
const filter4 = createFilter(ViewFilterOperand.IsNotEmpty, '', '');
|
||||
const filter5 = createFilter(ViewFilterOperand.IsNotEmpty, [], '');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@@ -235,21 +332,193 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('date operands', () => {
|
||||
it('should handle date operands (returning false as placeholder)', () => {
|
||||
const dateOperands = [
|
||||
ViewFilterOperand.IsRelative,
|
||||
const now = new Date();
|
||||
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
||||
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
|
||||
const today = new Date();
|
||||
|
||||
it('should handle IsInPast operand correctly', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsInPast,
|
||||
pastDate,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
const futureFilter = createFilter(
|
||||
ViewFilterOperand.IsInPast,
|
||||
futureDate,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [futureFilter] })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IsInFuture operand correctly', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsInFuture,
|
||||
futureDate,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
const pastFilter = createFilter(
|
||||
ViewFilterOperand.IsInFuture,
|
||||
pastDate,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [pastFilter] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle IsToday operand correctly', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsToday,
|
||||
today,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
const pastFilter = createFilter(
|
||||
ViewFilterOperand.IsToday,
|
||||
pastDate,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [pastFilter] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle IsBefore operand correctly', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsBefore,
|
||||
pastDate,
|
||||
now,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
const futureFilter = createFilter(
|
||||
ViewFilterOperand.IsBefore,
|
||||
futureDate,
|
||||
now,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [futureFilter] })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IsAfter operand correctly', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.IsAfter,
|
||||
];
|
||||
futureDate,
|
||||
now,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
dateOperands.forEach((operand) => {
|
||||
const filter = createFilter(operand, new Date(), new Date());
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(false);
|
||||
});
|
||||
const pastFilter = createFilter(
|
||||
ViewFilterOperand.IsAfter,
|
||||
pastDate,
|
||||
now,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [pastFilter] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Is operand for dates correctly', () => {
|
||||
const sameDate1 = new Date('2023-01-15');
|
||||
const sameDate2 = new Date('2023-01-15');
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
sameDate1,
|
||||
sameDate2,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
|
||||
const differentFilter = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
sameDate1,
|
||||
now,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [differentFilter] })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle date IsEmpty and IsNotEmpty operands', () => {
|
||||
const emptyFilter = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
null,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [emptyFilter] })).toBe(true);
|
||||
|
||||
const notEmptyFilter = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
now,
|
||||
null,
|
||||
'DATE',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [notEmptyFilter] })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currency operands', () => {
|
||||
it('should handle currency code operands', () => {
|
||||
const filter: ResolvedFilter = {
|
||||
id: 'filter1',
|
||||
type: 'CURRENCY',
|
||||
label: 'Currency Filter',
|
||||
rightOperand: 'USD',
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'USD',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'USD',
|
||||
compositeFieldSubFieldName: 'currencyCode',
|
||||
};
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle currency amount operands', () => {
|
||||
const filter: ResolvedFilter = {
|
||||
id: 'filter1',
|
||||
type: 'CURRENCY',
|
||||
label: 'Currency Filter',
|
||||
rightOperand: 100,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '100',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 150,
|
||||
compositeFieldSubFieldName: 'amountMicros',
|
||||
};
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,10 +528,24 @@ describe('evaluateFilterConditions', () => {
|
||||
'unknown' as ViewFilterOperand,
|
||||
'value',
|
||||
'value',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for unsupported filter type', () => {
|
||||
const filter = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
'value',
|
||||
'value',
|
||||
'UNSUPPORTED_TYPE',
|
||||
);
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||
'Filter type UNSUPPORTED_TYPE not supported',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,7 +554,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -281,7 +564,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -300,7 +583,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -310,7 +593,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -339,7 +622,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -349,7 +632,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -375,7 +658,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -385,7 +668,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -413,7 +696,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -423,7 +706,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -449,7 +732,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -459,7 +742,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -491,7 +774,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -501,7 +784,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -541,7 +824,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Filter 1',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -551,7 +834,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Filter 2',
|
||||
rightOperand: 'Smith',
|
||||
operand: ViewFilterOperand.Is,
|
||||
@@ -561,7 +844,7 @@ describe('evaluateFilterConditions', () => {
|
||||
},
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'number',
|
||||
type: 'NUMBER',
|
||||
label: 'Filter 3',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
@@ -604,7 +887,7 @@ describe('evaluateFilterConditions', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
type: 'RELATION',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
|
||||
+201
-123
@@ -1,9 +1,9 @@
|
||||
import { isString } from '@sniptt/guards';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
@@ -11,129 +11,33 @@ type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
};
|
||||
|
||||
function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||
const leftValue = filter.leftOperand;
|
||||
const rightValue = filter.rightOperand;
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
switch (typeof leftValue) {
|
||||
case 'string':
|
||||
return (
|
||||
String(leftValue).toLowerCase() === String(rightValue).toLowerCase()
|
||||
);
|
||||
case 'boolean':
|
||||
return Boolean(leftValue) === Boolean(rightValue);
|
||||
default:
|
||||
return leftValue === rightValue;
|
||||
}
|
||||
|
||||
case ViewFilterOperand.IsNot:
|
||||
return String(leftValue) !== String(rightValue);
|
||||
|
||||
case ViewFilterOperand.GreaterThanOrEqual:
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.LessThanOrEqual:
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.Contains:
|
||||
if (Array.isArray(leftValue)) {
|
||||
try {
|
||||
const parsedRightValue = Array.isArray(rightValue)
|
||||
? rightValue
|
||||
: JSON.parse(rightValue as string);
|
||||
|
||||
if (Array.isArray(parsedRightValue)) {
|
||||
return parsedRightValue.every((item) => leftValue.includes(item));
|
||||
} else {
|
||||
return leftValue.includes(parsedRightValue);
|
||||
}
|
||||
} catch (error) {
|
||||
return leftValue.includes(rightValue);
|
||||
}
|
||||
}
|
||||
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (Array.isArray(leftValue)) {
|
||||
try {
|
||||
const parsedRightValue = Array.isArray(rightValue)
|
||||
? rightValue
|
||||
: JSON.parse(rightValue as string);
|
||||
|
||||
if (Array.isArray(parsedRightValue)) {
|
||||
return !parsedRightValue.every((item) => leftValue.includes(item));
|
||||
} else {
|
||||
return !leftValue.includes(parsedRightValue);
|
||||
}
|
||||
} catch (error) {
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
}
|
||||
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
return (
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
leftValue === '' ||
|
||||
(Array.isArray(leftValue) && leftValue.length === 0)
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return (
|
||||
leftValue !== null &&
|
||||
leftValue !== undefined &&
|
||||
leftValue !== '' &&
|
||||
(!Array.isArray(leftValue) || leftValue.length > 0)
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotNull:
|
||||
return leftValue !== null && leftValue !== undefined;
|
||||
|
||||
case ViewFilterOperand.IsInPast:
|
||||
if (typeof leftValue === 'string') {
|
||||
return Date.now() - new Date(leftValue).getTime() > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.IsInFuture:
|
||||
if (typeof leftValue === 'string') {
|
||||
return Date.now() - new Date(leftValue).getTime() < 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.IsToday:
|
||||
if (typeof leftValue === 'string') {
|
||||
return new Date(leftValue).toDateString() === new Date().toDateString();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.IsBefore:
|
||||
if (typeof leftValue === 'string' && typeof rightValue === 'string') {
|
||||
return new Date(leftValue).getTime() < new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.IsAfter:
|
||||
if (typeof leftValue === 'string' && typeof rightValue === 'string') {
|
||||
return new Date(leftValue).getTime() > new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.VectorSearch:
|
||||
case ViewFilterOperand.IsRelative:
|
||||
return false;
|
||||
|
||||
switch (filter.type) {
|
||||
case 'NUMBER':
|
||||
case 'NUMERIC':
|
||||
return evaluateNumberFilter(filter);
|
||||
case 'DATE':
|
||||
case 'DATE_TIME':
|
||||
return evaluateDateFilter(filter);
|
||||
case 'TEXT':
|
||||
case 'SELECT':
|
||||
case 'MULTI_SELECT':
|
||||
case 'FULL_NAME':
|
||||
case 'EMAILS':
|
||||
case 'PHONES':
|
||||
case 'ADDRESS':
|
||||
case 'LINKS':
|
||||
case 'ARRAY':
|
||||
case 'RAW_JSON':
|
||||
return evaluateTextAndArrayFilter(filter);
|
||||
case 'BOOLEAN':
|
||||
return evaluateBooleanFilter(filter);
|
||||
case 'UUID':
|
||||
case 'RELATION':
|
||||
return evaluateRelationFilter(filter);
|
||||
case 'CURRENCY':
|
||||
return evaluateCurrencyFilter(filter);
|
||||
default:
|
||||
assertUnreachable(filter.operand);
|
||||
throw new Error(`Filter type ${filter.type} not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +85,180 @@ function evaluateFilterGroup(
|
||||
}
|
||||
}
|
||||
|
||||
function contains(leftValue: unknown, rightValue: unknown): boolean {
|
||||
// if two arrays, check if any item is in the other array
|
||||
if (Array.isArray(leftValue) && Array.isArray(rightValue)) {
|
||||
return leftValue.some((item) => rightValue.includes(item));
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(leftValue) || isString(leftValue)) &&
|
||||
isString(rightValue)
|
||||
) {
|
||||
try {
|
||||
const parsedRightValue = JSON.parse(rightValue as string);
|
||||
|
||||
if (Array.isArray(parsedRightValue)) {
|
||||
return parsedRightValue.some((item) => leftValue.includes(item));
|
||||
} else {
|
||||
return leftValue.includes(parsedRightValue);
|
||||
}
|
||||
} catch (error) {
|
||||
return leftValue.includes(rightValue);
|
||||
}
|
||||
}
|
||||
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
}
|
||||
|
||||
function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return contains(filter.leftOperand, filter.rightOperand);
|
||||
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)
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return (
|
||||
filter.leftOperand !== null &&
|
||||
filter.leftOperand !== undefined &&
|
||||
filter.leftOperand !== '' &&
|
||||
(!Array.isArray(filter.leftOperand) || filter.leftOperand.length > 0)
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for this filter type`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateBooleanFilter(filter: ResolvedFilter): boolean {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return Boolean(filter.leftOperand) === Boolean(filter.rightOperand);
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for boolean filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateDateFilter(filter: ResolvedFilter): boolean {
|
||||
const dateLeftValue = new Date(String(filter.leftOperand));
|
||||
const dateRightValue = new Date(String(filter.rightOperand));
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return dateLeftValue.getDate() === dateRightValue.getDate();
|
||||
case ViewFilterOperand.IsInPast:
|
||||
return dateLeftValue.getTime() < Date.now();
|
||||
|
||||
case ViewFilterOperand.IsInFuture:
|
||||
return dateLeftValue.getTime() > Date.now();
|
||||
|
||||
case ViewFilterOperand.IsToday:
|
||||
return dateLeftValue.toDateString() === new Date().toDateString();
|
||||
|
||||
case ViewFilterOperand.IsBefore:
|
||||
return dateLeftValue.getTime() < dateRightValue.getTime();
|
||||
|
||||
case ViewFilterOperand.IsAfter:
|
||||
return dateLeftValue.getTime() > dateRightValue.getTime();
|
||||
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
return (
|
||||
filter.leftOperand === null ||
|
||||
filter.leftOperand === undefined ||
|
||||
filter.leftOperand === ''
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return (
|
||||
filter.leftOperand !== null &&
|
||||
filter.leftOperand !== undefined &&
|
||||
filter.leftOperand !== ''
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for date filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateRelationFilter(filter: ResolvedFilter): boolean {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return filter.leftOperand === filter.rightOperand;
|
||||
case ViewFilterOperand.IsNot:
|
||||
return filter.leftOperand !== filter.rightOperand;
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for relation filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateCurrencyFilter(filter: ResolvedFilter): boolean {
|
||||
if (filter.compositeFieldSubFieldName === 'currencyCode') {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return filter.leftOperand === filter.rightOperand;
|
||||
case ViewFilterOperand.IsNot:
|
||||
return filter.leftOperand !== filter.rightOperand;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
return (
|
||||
filter.leftOperand === null ||
|
||||
filter.leftOperand === undefined ||
|
||||
filter.leftOperand === ''
|
||||
);
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return (
|
||||
filter.leftOperand !== null &&
|
||||
filter.leftOperand !== undefined &&
|
||||
filter.leftOperand !== ''
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for currency filter`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return evaluateNumberFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateNumberFilter(filter: ResolvedFilter): boolean {
|
||||
const leftValue = filter.leftOperand;
|
||||
const rightValue = filter.rightOperand;
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.GreaterThanOrEqual:
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.LessThanOrEqual:
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
return leftValue === null || leftValue === undefined || leftValue === '';
|
||||
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return leftValue !== null && leftValue !== undefined && leftValue !== '';
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Operand ${filter.operand} not supported for number filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateFilterConditions({
|
||||
filterGroups = [],
|
||||
filters = [],
|
||||
|
||||
Reference in New Issue
Block a user