Handle relations for filters (#13654)

- Refacto variable dropdown so it can display both objects and fields
- we do not filter on object name anymore to simplify the code
- add relation handler in filters



https://github.com/user-attachments/assets/e4f03f11-45cb-4d3f-b628-e996129dd996
This commit is contained in:
Thomas Trompette
2025-08-06 14:57:10 +02:00
committed by GitHub
parent d74bcbe62b
commit d9dbfc4f7e
21 changed files with 705 additions and 307 deletions
@@ -95,6 +95,223 @@ describe('evaluateFilterConditions', () => {
expect(result).toBe(true);
});
// Enhanced relation filter tests with object id extraction
it('should extract id from left operand object for relation comparison', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const leftObject = { id: uuid1, name: 'John Doe' };
const rightValue = uuid1;
const filter = createFilter(
ViewFilterOperand.Is,
leftObject,
rightValue,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should extract id from right operand object for relation comparison', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const leftValue = uuid1;
const rightObject = { id: uuid1, name: 'John Doe' };
const filter = createFilter(
ViewFilterOperand.Is,
leftValue,
rightObject,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should extract id from both operands when they are objects for relation comparison', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const leftObject = { id: uuid1, name: 'John Doe' };
const rightObject = { id: uuid1, title: 'Admin' };
const filter = createFilter(
ViewFilterOperand.Is,
leftObject,
rightObject,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should return false when extracted ids do not match for relation comparison', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const leftObject = { id: uuid1, name: 'John Doe' };
const rightObject = { id: uuid2, name: 'Jane Smith' };
const filter = createFilter(
ViewFilterOperand.Is,
leftObject,
rightObject,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false);
});
it('should handle IsNot with object id extraction for relation comparison', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const leftObject = { id: uuid1, name: 'John Doe' };
const rightObject = { id: uuid2, name: 'Jane Smith' };
const filter = createFilter(
ViewFilterOperand.IsNot,
leftObject,
rightObject,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should handle objects without id property for relation comparison', () => {
const leftObject = { name: 'John Doe' };
const rightObject = { name: 'John Doe' };
const filter = createFilter(
ViewFilterOperand.Is,
leftObject,
rightObject,
'RELATION',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false); // Objects are different references
});
it('should throw error for unsupported relation filter operand', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const filter = createFilter(
ViewFilterOperand.Contains,
uuid1,
uuid2,
'RELATION',
);
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
'Operand contains not supported for relation filter',
);
});
});
describe('UUID filter operands', () => {
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
it('should return true when UUIDs are equal (Is)', () => {
const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid1, 'UUID');
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should return false when UUIDs are not equal (Is)', () => {
const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid2, 'UUID');
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false);
});
it('should return false when UUIDs are equal (IsNot)', () => {
const filter = createFilter(
ViewFilterOperand.IsNot,
uuid1,
uuid1,
'UUID',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(false);
});
it('should return true when UUIDs are not equal (IsNot)', () => {
const filter = createFilter(
ViewFilterOperand.IsNot,
uuid1,
uuid2,
'UUID',
);
const result = evaluateFilterConditions({ filters: [filter] });
expect(result).toBe(true);
});
it('should handle null/undefined UUIDs with Is operand', () => {
const filter1 = createFilter(ViewFilterOperand.Is, null, null, 'UUID');
const filter2 = createFilter(
ViewFilterOperand.Is,
undefined,
undefined,
'UUID',
);
const filter3 = createFilter(ViewFilterOperand.Is, uuid1, null, 'UUID');
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
});
it('should handle null/undefined UUIDs with IsNot operand', () => {
const filter1 = createFilter(
ViewFilterOperand.IsNot,
null,
null,
'UUID',
);
const filter2 = createFilter(
ViewFilterOperand.IsNot,
undefined,
undefined,
'UUID',
);
const filter3 = createFilter(
ViewFilterOperand.IsNot,
uuid1,
null,
'UUID',
);
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
});
it('should handle empty string UUIDs', () => {
const filter1 = createFilter(ViewFilterOperand.Is, '', '', 'UUID');
const filter2 = createFilter(ViewFilterOperand.Is, uuid1, '', 'UUID');
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
});
it('should throw error for unsupported UUID filter operand', () => {
const filter = createFilter(
ViewFilterOperand.Contains,
uuid1,
uuid2,
'UUID',
);
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
'Operand contains not supported for uuid filter',
);
});
});
describe('Boolean filter operands', () => {
@@ -1,4 +1,4 @@
import { isString } from '@sniptt/guards';
import { isObject, isString } from '@sniptt/guards';
import {
StepFilter,
StepFilterGroup,
@@ -32,6 +32,7 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
case 'BOOLEAN':
return evaluateBooleanFilter(filter);
case 'UUID':
return evaluateUuidFilter(filter);
case 'RELATION':
return evaluateRelationFilter(filter);
case 'CURRENCY':
@@ -193,12 +194,36 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean {
}
}
function evaluateRelationFilter(filter: ResolvedFilter): boolean {
function evaluateUuidFilter(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 uuid filter`,
);
}
}
function evaluateRelationFilter(filter: ResolvedFilter): boolean {
// compare only the ids. If the left operand is the relation object, get the id
const leftValue =
isObject(filter.leftOperand) && 'id' in filter.leftOperand
? filter.leftOperand.id
: filter.leftOperand;
const rightValue =
isObject(filter.rightOperand) && 'id' in filter.rightOperand
? filter.rightOperand.id
: filter.rightOperand;
switch (filter.operand) {
case ViewFilterOperand.Is:
return leftValue === rightValue;
case ViewFilterOperand.IsNot:
return leftValue !== rightValue;
default:
throw new Error(
`Operand ${filter.operand} not supported for relation filter`,