Null equivalence - update filter (#16123)
This commit is contained in:
+56
@@ -501,8 +501,24 @@ describe('evaluateFilterConditions', () => {
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.CONTAINS,
|
||||
null,
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
const filter4 = createFilter(
|
||||
ViewFilterOperand.CONTAINS,
|
||||
'',
|
||||
null,
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle DoesNotContain operand with strings', () => {
|
||||
@@ -519,8 +535,16 @@ describe('evaluateFilterConditions', () => {
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.DOES_NOT_CONTAIN,
|
||||
null,
|
||||
'',
|
||||
'TEXT',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Contains operand with arrays', () => {
|
||||
@@ -537,8 +561,32 @@ describe('evaluateFilterConditions', () => {
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.CONTAINS,
|
||||
null,
|
||||
[],
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
const filter4 = createFilter(
|
||||
ViewFilterOperand.CONTAINS,
|
||||
[],
|
||||
null,
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
const filter5 = createFilter(
|
||||
ViewFilterOperand.CONTAINS,
|
||||
null,
|
||||
['apple'],
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter5] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle DoesNotContain operand with arrays', () => {
|
||||
@@ -555,8 +603,16 @@ describe('evaluateFilterConditions', () => {
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.DOES_NOT_CONTAIN,
|
||||
null,
|
||||
['apple'],
|
||||
'ARRAY',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
import { findDefaultNullEquivalentValue } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/find-default-null-equivalent-value.util';
|
||||
|
||||
describe('findDefaultNullEquivalentValue', () => {
|
||||
describe('Simple Types', () => {
|
||||
describe('TEXT', () => {
|
||||
it('should return DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE for null', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: null,
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE for empty string', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return undefined for non-null equivalent value', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: 'value',
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ARRAY', () => {
|
||||
it('should return DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE for null', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: null,
|
||||
fieldMetadataType: FieldMetadataType.ARRAY,
|
||||
}),
|
||||
).toBe(DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE for empty array', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: [],
|
||||
fieldMetadataType: FieldMetadataType.ARRAY,
|
||||
}),
|
||||
).toBe(DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACTOR', () => {
|
||||
it('should return text default for name', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.ACTOR,
|
||||
key: 'name',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return undefined for context', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: {},
|
||||
fieldMetadataType: FieldMetadataType.ACTOR,
|
||||
key: 'context',
|
||||
}),
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADDRESS', () => {
|
||||
it.each([
|
||||
'addressStreet1',
|
||||
'addressStreet2',
|
||||
'addressCity',
|
||||
'addressState',
|
||||
'addressPostcode',
|
||||
'addressCountry',
|
||||
])('should return text default for %s', (key) => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.ADDRESS,
|
||||
key,
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EMAILS', () => {
|
||||
it('should return text default for primaryEmail', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
key: 'primaryEmail',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return array default for additionalEmails', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: [],
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
key: 'additionalEmails',
|
||||
}),
|
||||
).toBe(DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LINKS', () => {
|
||||
it('should return text default for primaryLinkUrl', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.LINKS,
|
||||
key: 'primaryLinkUrl',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return text default for primaryLinkLabel', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.LINKS,
|
||||
key: 'primaryLinkLabel',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHONES', () => {
|
||||
it('should return text default for primaryPhoneNumber', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.PHONES,
|
||||
key: 'primaryPhoneNumber',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return text default for primaryPhoneCountryCode', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.PHONES,
|
||||
key: 'primaryPhoneCountryCode',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return text default for primaryPhoneCallingCode', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.PHONES,
|
||||
key: 'primaryPhoneCallingCode',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RICH_TEXT_V2', () => {
|
||||
it('should return undefined for blocknote', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: {},
|
||||
fieldMetadataType: FieldMetadataType.RICH_TEXT_V2,
|
||||
key: 'blocknote',
|
||||
}),
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return text default for markdown', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.RICH_TEXT_V2,
|
||||
key: 'markdown',
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FULL_NAME', () => {
|
||||
it.each(['firstName', 'lastName'])(
|
||||
'should return text default for %s',
|
||||
(key) => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.FULL_NAME,
|
||||
key,
|
||||
}),
|
||||
).toBe(DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for unknown type', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: null,
|
||||
fieldMetadataType: 'UNKNOWN' as any,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown composite key', () => {
|
||||
expect(
|
||||
findDefaultNullEquivalentValue({
|
||||
value: '',
|
||||
fieldMetadataType: FieldMetadataType.ACTOR,
|
||||
key: 'unknown',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
+32
-5
@@ -10,9 +10,13 @@ import {
|
||||
ViewFilterOperand,
|
||||
type ViewFilterOperandDeprecated,
|
||||
} from 'twenty-shared/types';
|
||||
import { convertViewFilterOperandToCoreOperand as convertViewFilterOperandDeprecated } from 'twenty-shared/utils';
|
||||
import {
|
||||
convertViewFilterOperandToCoreOperand as convertViewFilterOperandDeprecated,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { parseBooleanFromStringValue } from 'twenty-shared/workflow';
|
||||
|
||||
import { findDefaultNullEquivalentValue } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/find-default-null-equivalent-value.util';
|
||||
import { parseAndEvaluateRelativeDateFilter } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util';
|
||||
|
||||
type ResolvedFilterWithPotentiallyDeprecatedOperand = Omit<
|
||||
@@ -59,7 +63,11 @@ function evaluateFilter(
|
||||
case 'ARRAY':
|
||||
case 'array':
|
||||
case 'RAW_JSON':
|
||||
return evaluateTextAndArrayFilter(filterWithConvertedOperand);
|
||||
return evaluateTextAndArrayFilter(
|
||||
filterWithConvertedOperand,
|
||||
filter.type,
|
||||
filter.compositeFieldSubFieldName,
|
||||
);
|
||||
case 'SELECT':
|
||||
return evaluateSelectFilter(filterWithConvertedOperand);
|
||||
case 'BOOLEAN':
|
||||
@@ -146,12 +154,31 @@ function contains(leftValue: unknown, rightValue: unknown): boolean {
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
}
|
||||
|
||||
function evaluateTextAndArrayFilter(filter: ResolvedFilter): boolean {
|
||||
function evaluateTextAndArrayFilter(
|
||||
filter: ResolvedFilter,
|
||||
filterType: string,
|
||||
compositeFieldSubFieldName: string | undefined,
|
||||
): boolean {
|
||||
//TODO : nullEquivalentRightValue to remove once feature flag removed + workflow action based on common api
|
||||
const nullEquivalentRightValue = findDefaultNullEquivalentValue({
|
||||
value: filter.rightOperand,
|
||||
fieldMetadataType: filterType,
|
||||
key: compositeFieldSubFieldName,
|
||||
});
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.CONTAINS:
|
||||
return contains(filter.leftOperand, filter.rightOperand);
|
||||
return (
|
||||
contains(filter.leftOperand, filter.rightOperand) ||
|
||||
(isDefined(nullEquivalentRightValue) &&
|
||||
!isNotEmptyTextOrArray(filter.leftOperand))
|
||||
);
|
||||
case ViewFilterOperand.DOES_NOT_CONTAIN:
|
||||
return !contains(filter.leftOperand, filter.rightOperand);
|
||||
return (
|
||||
!contains(filter.leftOperand, filter.rightOperand) ||
|
||||
(isDefined(nullEquivalentRightValue) &&
|
||||
isNotEmptyTextOrArray(filter.leftOperand))
|
||||
);
|
||||
case ViewFilterOperand.IS_EMPTY:
|
||||
return !isNotEmptyTextOrArray(filter.leftOperand);
|
||||
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
import { isNullEquivalentTextFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-text-field-value.util';
|
||||
|
||||
export const findDefaultNullEquivalentValue = ({
|
||||
value,
|
||||
fieldMetadataType,
|
||||
key,
|
||||
}: {
|
||||
value: unknown;
|
||||
fieldMetadataType: string;
|
||||
key?: string;
|
||||
}) => {
|
||||
switch (fieldMetadataType) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
case FieldMetadataType.ARRAY:
|
||||
return isNullEquivalentArrayFieldValue(value)
|
||||
? DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case FieldMetadataType.ACTOR: {
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.ADDRESS: {
|
||||
switch (key) {
|
||||
case 'addressStreet1':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressStreet2':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressCity':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressState':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressPostcode':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressCountry':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.EMAILS: {
|
||||
switch (key) {
|
||||
case 'primaryEmail':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'additionalEmails':
|
||||
return isNullEquivalentArrayFieldValue(value)
|
||||
? DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.LINKS: {
|
||||
switch (key) {
|
||||
case 'primaryLinkUrl':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryLinkLabel':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'secondaryLinks':
|
||||
return isNullEquivalentArrayFieldValue(value)
|
||||
? DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.PHONES: {
|
||||
switch (key) {
|
||||
case 'primaryPhoneNumber':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryPhoneCountryCode':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryPhoneCallingCode':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'additionalPhones':
|
||||
return isNullEquivalentArrayFieldValue(value)
|
||||
? DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
switch (key) {
|
||||
case 'markdown':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.FULL_NAME: {
|
||||
switch (key) {
|
||||
case 'firstName':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'lastName':
|
||||
return isNullEquivalentTextFieldValue(value)
|
||||
? DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
Reference in New Issue
Block a user