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:
+1
@@ -40,6 +40,7 @@ export type RecordOutputSchema = {
|
||||
nameSingular: string;
|
||||
fieldIdName: string;
|
||||
objectMetadataId: string;
|
||||
isRelationField?: boolean;
|
||||
} & Leaf;
|
||||
fields: Record<string, FieldOutputSchema>;
|
||||
_outputSchemaType: 'RECORD';
|
||||
|
||||
+73
-81
@@ -56,92 +56,84 @@ describe('generateFakeFormResponse', () => {
|
||||
objectMetadataMaps: mockObjectMetadataMaps,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"age": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Age",
|
||||
"type": "NUMBER",
|
||||
"value": 20,
|
||||
},
|
||||
"company": {
|
||||
"isLeaf": false,
|
||||
"label": "Company",
|
||||
"value": {
|
||||
"_outputSchemaType": "RECORD",
|
||||
"fields": {
|
||||
"domainName": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"icon": "test-field-icon",
|
||||
"isLeaf": false,
|
||||
"label": "Domain Name",
|
||||
"type": "LINKS",
|
||||
"value": {
|
||||
"primaryLinkLabel": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Primary Link Label",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
expect(result).toEqual({
|
||||
age: {
|
||||
isLeaf: true,
|
||||
label: 'Age',
|
||||
type: 'NUMBER',
|
||||
value: 20,
|
||||
},
|
||||
company: {
|
||||
isLeaf: false,
|
||||
label: 'Company',
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
domainName: {
|
||||
fieldMetadataId: 'domainNameFieldMetadataId',
|
||||
icon: 'test-field-icon',
|
||||
isLeaf: false,
|
||||
label: 'Domain Name',
|
||||
type: 'LINKS',
|
||||
value: {
|
||||
primaryLinkLabel: {
|
||||
fieldMetadataId: 'domainNameFieldMetadataId',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
label: 'Primary Link Label',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
primaryLinkUrl: {
|
||||
fieldMetadataId: 'domainNameFieldMetadataId',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
label: 'Primary Link Url',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
secondaryLinks: {
|
||||
fieldMetadataId: 'domainNameFieldMetadataId',
|
||||
isCompositeSubField: true,
|
||||
isLeaf: true,
|
||||
label: 'Secondary Links',
|
||||
type: 'RAW_JSON',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"primaryLinkUrl": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Primary Link Url",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
},
|
||||
"secondaryLinks": {
|
||||
"fieldMetadataId": "domainNameFieldMetadataId",
|
||||
"isCompositeSubField": true,
|
||||
"isLeaf": true,
|
||||
"label": "Secondary Links",
|
||||
"type": "RAW_JSON",
|
||||
"value": null,
|
||||
name: {
|
||||
fieldMetadataId: 'nameFieldMetadataId',
|
||||
icon: 'test-field-icon',
|
||||
isLeaf: true,
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
"fieldMetadataId": "nameFieldMetadataId",
|
||||
"icon": "test-field-icon",
|
||||
"isLeaf": true,
|
||||
"label": "Name",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
object: {
|
||||
fieldIdName: 'id',
|
||||
icon: 'test-company-icon',
|
||||
isLeaf: true,
|
||||
label: 'Company',
|
||||
nameSingular: 'company',
|
||||
objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c',
|
||||
value: 'A company',
|
||||
},
|
||||
},
|
||||
},
|
||||
"object": {
|
||||
"fieldIdName": "id",
|
||||
"icon": "test-company-icon",
|
||||
"isLeaf": true,
|
||||
"label": "Company",
|
||||
"nameSingular": "company",
|
||||
"objectMetadataId": "20202020-c03c-45d6-a4b0-04afe1357c5c",
|
||||
"value": "A company",
|
||||
date: {
|
||||
isLeaf: true,
|
||||
label: 'Date',
|
||||
type: 'DATE',
|
||||
value: 'mm/dd/yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
"date": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Date",
|
||||
"type": "DATE",
|
||||
"value": "mm/dd/yyyy",
|
||||
},
|
||||
"name": {
|
||||
"fieldMetadataId": undefined,
|
||||
"icon": undefined,
|
||||
"isLeaf": true,
|
||||
"label": "Name",
|
||||
"type": "TEXT",
|
||||
"value": "My text",
|
||||
},
|
||||
}
|
||||
`);
|
||||
name: {
|
||||
isLeaf: true,
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
value: 'My text',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
@@ -5,9 +5,11 @@ import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builde
|
||||
export const generateFakeObjectRecord = ({
|
||||
objectMetadataInfo,
|
||||
depth = 0,
|
||||
isRelationField,
|
||||
}: {
|
||||
objectMetadataInfo: ObjectMetadataInfo;
|
||||
depth?: number;
|
||||
isRelationField?: boolean;
|
||||
}): RecordOutputSchema => {
|
||||
return {
|
||||
object: {
|
||||
@@ -20,6 +22,7 @@ export const generateFakeObjectRecord = ({
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
fieldIdName: 'id',
|
||||
objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id,
|
||||
isRelationField,
|
||||
},
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo,
|
||||
|
||||
+2
@@ -52,6 +52,7 @@ export const generateObjectRecordFields = ({
|
||||
isLeaf: false,
|
||||
icon: field.icon ?? undefined,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
fieldMetadataId: field.id,
|
||||
value: generateFakeObjectRecord({
|
||||
objectMetadataInfo: {
|
||||
@@ -59,6 +60,7 @@ export const generateObjectRecordFields = ({
|
||||
objectMetadataMaps: objectMetadataInfo.objectMetadataMaps,
|
||||
},
|
||||
depth: depth + 1,
|
||||
isRelationField: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+217
@@ -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', () => {
|
||||
|
||||
+27
-2
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user