From 3ed11054a0ff916a6de51fef576f14f97a591762 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 31 Jul 2026 11:14:19 +0200 Subject: [PATCH] Keep leading + when filtering phones by calling code (#23546) Fixes #23528 Filtering a PHONES field with `CONTAINS` / `DOES_NOT_CONTAIN` stripped every non-digit character from the filter value, so `+33` became `33` and the generated `ilike`/`like` predicates could not distinguish an international calling code from any number containing those digits. `turnRecordFilterIntoGqlOperationFilter` now preserves a leading `+` while still removing other formatting characters (spaces, dashes, parentheses). `+33 6 12` becomes `+33612`; values without a `+` are unchanged. Added a regression test in `computeViewRecordGqlOperationFilter.test.ts` for a `+`-prefixed value. Lint and typecheck pass on `twenty-shared` and `twenty-front`; the filter test suites pass in both packages. --------- Co-authored-by: Thomas Trompette --- ...omputeViewRecordGqlOperationFilter.test.ts | 54 +++++++++++++++++++ .../turnRecordFilterIntoGqlOperationFilter.ts | 10 +++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index 5a2663d6ca..9a52e51382 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -799,6 +799,60 @@ describe('should work as expected for the different field types', () => { }); }); + it('phones field type with an international calling code prefix', () => { + const personMockPhonesFieldMetadataItem = + personMockObjectMetadataItem.fields.find( + (field) => field.name === 'phones', + ); + + if (!isDefined(personMockPhonesFieldMetadataItem)) { + throw new Error('Person mock phones field metadata ID is undefined'); + } + + const phonesFilterContains: RecordFilter = { + id: 'person-phones-filter-contains-calling-code', + value: '+33 6 12', + fieldMetadataId: personMockPhonesFieldMetadataItem.id, + displayValue: '+33 6 12', + operand: ViewFilterOperand.CONTAINS, + label: 'Phones', + type: FieldMetadataType.PHONES, + }; + + const result = computeRecordGqlOperationFilter({ + filterValueDependencies: mockFilterValueDependencies, + recordFilters: [phonesFilterContains], + recordFilterGroups: [], + fieldMetadataItems: personFields, + }); + + expect(result).toEqual({ + or: [ + { + phones: { + primaryPhoneNumber: { + ilike: '%+33612%', + }, + }, + }, + { + phones: { + primaryPhoneCallingCode: { + ilike: '%+33612%', + }, + }, + }, + { + phones: { + additionalPhones: { + like: '%+33612%', + }, + }, + }, + ], + }); + }); + it('emails field type', () => { const personMockEmailFieldMetadataId = getMockFieldMetadataItemOrThrow({ objectMetadataItem: personMockObjectMetadataItem, diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index 9dca576731..812882af45 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -60,6 +60,10 @@ import { computeRelationGqlFieldJoinColumnName, } from '@/utils/fieldMetadata/compute-relation-gql-field-join-column-name'; +const PHONE_FILTER_NON_SIGNIFICANT_CHARS = /(?!^)\+|[^0-9+]/g; + +const CONTAINS_DIGIT = /[0-9]/; + type FieldSharedMorphRelation = { type: RelationType; targetObjectMetadata: { @@ -1403,9 +1407,11 @@ const buildDirectFieldGqlOperationFilter = ({ } case 'PHONES': { if (!isSubFieldFilter) { - const filterValue = recordFilter.value.replace(/[^0-9]/g, ''); + const filterValue = recordFilter.value + .trim() + .replace(PHONE_FILTER_NON_SIGNIFICANT_CHARS, ''); - if (!isNonEmptyString(filterValue)) { + if (!CONTAINS_DIGIT.test(filterValue)) { return; }