From df3db85f7f3743dffa28a3cce8b9eca27517fbef Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 12 May 2025 21:30:53 +0200 Subject: [PATCH] Implemented LINKS and EMAILS sub-field fitering (#11984) This PR introduces LINKS and EMAILS sub-field filtering. It's mainly about the implementation of secondaryLinks and additionalEmails sub-fields, which are treated like additionalPhones. There's also a refactor on the computeViewRecordGqlOperationFilter, a big file that becomes very difficult to read and maintain. This PR breaks it down into multiple smaller utils. There's still work to be done to clean it as it is a central part of the record filter module, this PR lays the foundation. --- .../utils/computeContextStoreFilters.ts | 2 +- .../AdvancedFilterSubFieldSelectMenu.tsx | 6 +- .../graphql/types/RecordGqlOperationFilter.ts | 2 + .../ObjectFilterDropdownTextInput.tsx | 2 +- .../getOperandsForFilterType.test.ts | 12 +- .../constants/IconNameBySubField.ts | 7 + ...omputeViewRecordGqlOperationFilter.test.ts | 111 ++- .../areCompositeTypeSubFieldsFilterable.ts | 2 + ...computeEmptyGqlOperationFilterForEmails.ts | 92 +++ .../computeEmptyGqlOperationFilterForLinks.ts | 122 ++++ .../checkIfShouldComputeEmptinessFilter.ts | 31 + .../checkIfShouldSkipFiltering.ts | 26 + .../computeGqlOperationFilterForEmails.ts | 153 ++++ .../computeGqlOperationFilterForLinks.ts | 166 +++++ ...RecordFilterGroupIntoGqlOperationFilter.ts | 83 +++ ...turnRecordFilterIntoGqlOperationFilter.ts} | 689 +++++++----------- .../utils/computeRecordGqlOperationFilter.ts | 66 ++ .../utils/getEmptyRecordGqlOperationFilter.ts | 53 +- .../utils/getRecordFilterOperands.ts | 5 +- .../useFindManyRecordIndexTableParams.ts | 2 +- .../hooks/useLoadRecordIndexBoardColumn.ts | 2 +- .../hooks/useAggregateRecordsForHeader.ts | 2 +- ...egateRecordsForRecordTableColumnFooter.tsx | 2 +- .../SettingsCompositeFieldTypeConfigs.ts | 8 +- .../internal/useGetRecordIndexTotalCount.ts | 2 +- .../views/utils/getQueryVariablesFromView.ts | 2 +- 26 files changed, 1129 insertions(+), 521 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldComputeEmptinessFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterGroupIntoGqlOperationFilter.ts rename packages/twenty-front/src/modules/object-record/record-filter/utils/{computeViewRecordGqlOperationFilter.ts => compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts} (60%) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/computeRecordGqlOperationFilter.ts diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index 3e888aa6d..73381a503 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -3,7 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; export const computeContextStoreFilters = ( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx index 60a484e16..95cafc345 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx @@ -90,9 +90,9 @@ export const AdvancedFilterSubFieldSelectMenu = ({ return null; } - const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ - objectFilterDropdownSubMenuFieldType - ].filterableSubFields.sort((a, b) => a.localeCompare(b)); + const subFieldNames = + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[objectFilterDropdownSubMenuFieldType] + .filterableSubFields; const subFieldsAreFilterable = isDefined(fieldMetadataItemUsedInDropdown) && diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 01433c9c6..28c0d6b45 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -91,6 +91,7 @@ export type AddressFilter = { export type LinksFilter = { primaryLinkUrl?: StringFilter; primaryLinkLabel?: StringFilter; + secondaryLinks?: RawJsonFilter; }; export type ActorFilter = { @@ -100,6 +101,7 @@ export type ActorFilter = { export type EmailsFilter = { primaryEmail?: StringFilter; + additionalEmails?: RawJsonFilter; }; export type PhonesFilter = { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx index 4c39c6892..fc23a0e87 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx @@ -38,7 +38,7 @@ export const ObjectFilterDropdownTextInput = () => { }; return ( - + { RecordFilterOperand.IsNot, ]; + const actorSourceOperands = [ + RecordFilterOperand.Is, + RecordFilterOperand.IsNot, + ]; + const dateOperands = [ RecordFilterOperand.Is, RecordFilterOperand.IsRelative, @@ -49,7 +54,12 @@ describe('getOperandsForFilterType', () => { ['FULL_NAME', [...containsOperands, ...emptyOperands]], ['ADDRESS', [...containsOperands, ...emptyOperands]], ['LINKS', [...containsOperands, ...emptyOperands]], - ['ACTOR', [...containsOperands, ...emptyOperands]], + ['LINKS', [...containsOperands, ...emptyOperands], 'primaryLinkUrl'], + ['LINKS', [...containsOperands, ...emptyOperands], 'primaryLinkLabel'], + ['LINKS', [...containsOperands, ...emptyOperands], 'secondaryLinks'], + ['ACTOR', [...containsOperands, ...emptyOperands], 'name'], + ['ACTOR', [...actorSourceOperands, ...emptyOperands], 'source'], + ['ACTOR', [...actorSourceOperands, ...emptyOperands]], [ 'CURRENCY', [...currencyCurrencyCodeOperands, ...emptyOperands], diff --git a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts index 1fdebd24e..b9b7ebba9 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts @@ -7,4 +7,11 @@ export const ICON_NAME_BY_SUB_FIELD: Partial< amountMicros: 'IconNumber95Small', name: 'IconAlignJustified', source: 'IconFileArrowLeft', + primaryEmail: 'IconMail', + additionalEmails: 'IconList', + primaryLinkLabel: 'IconLabel', + primaryLinkUrl: 'IconLink', + secondaryLinks: 'IconList', + primaryPhoneCallingCode: 'IconPlus', + additionalPhones: 'IconList', }; 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 11ddbba60..a472b15a3 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 @@ -2,7 +2,7 @@ import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMeta import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { FieldMetadataType } from '~/generated/graphql'; import { getCompaniesMock } from '~/testing/mock-data/companies'; @@ -682,7 +682,7 @@ describe('should work as expected for the different field types', () => { not: { phones: { additionalPhones: { - like: `%1234567890%`, + like: '%1234567890%', }, }, }, @@ -865,6 +865,13 @@ describe('should work as expected for the different field types', () => { }, }, }, + { + emails: { + additionalEmails: { + like: '%test@test.com%', + }, + }, + }, ], }, { @@ -878,42 +885,106 @@ describe('should work as expected for the different field types', () => { }, }, }, + { + or: [ + { + not: { + emails: { + additionalEmails: { + like: '%test@test.com%', + }, + }, + }, + }, + { + emails: { + additionalEmails: { + is: 'NULL', + }, + }, + }, + ], + }, ], }, { - or: [ + and: [ { - emails: { - primaryEmail: { - ilike: '', + or: [ + { + emails: { + primaryEmail: { + eq: '', + }, + }, }, - }, + { + emails: { + primaryEmail: { + is: 'NULL', + }, + }, + }, + ], }, { - emails: { - primaryEmail: { - is: 'NULL', + or: [ + { + emails: { + additionalEmails: { + is: 'NULL', + }, + }, }, - }, + { + emails: { + additionalEmails: { + like: '[]', + }, + }, + }, + ], }, ], }, { not: { - or: [ + and: [ { - emails: { - primaryEmail: { - ilike: '', + or: [ + { + emails: { + primaryEmail: { + eq: '', + }, + }, }, - }, + { + emails: { + primaryEmail: { + is: 'NULL', + }, + }, + }, + ], }, { - emails: { - primaryEmail: { - is: 'NULL', + or: [ + { + emails: { + additionalEmails: { + is: 'NULL', + }, + }, }, - }, + { + emails: { + additionalEmails: { + like: '[]', + }, + }, + }, + ], }, ], }, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts index 5205c3736..dad8ae7af 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts @@ -6,6 +6,8 @@ const COMPOSITE_TYPES_FILTERABLE = [ 'CURRENCY', 'ADDRESS', 'PHONES', + 'LINKS', + 'EMAILS', ] satisfies FieldType[]; type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number]; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails.ts new file mode 100644 index 000000000..167c70379 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails.ts @@ -0,0 +1,92 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { + EmailsFilter, + RecordGqlOperationFilter, +} from '@/object-record/graphql/types/RecordGqlOperationFilter'; + +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isNonEmptyString } from '@sniptt/guards'; + +export const computeEmptyGqlOperationFilterForEmails = ({ + recordFilter, + correspondingFieldMetadataItem, +}: { + recordFilter: RecordFilter; + correspondingFieldMetadataItem: Pick; +}): RecordGqlOperationFilter => { + const subFieldName = recordFilter.subFieldName; + const isSubFieldFilter = isNonEmptyString(subFieldName); + + if (isSubFieldFilter) { + switch (subFieldName) { + case 'primaryEmail': { + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { eq: '' }, + } satisfies EmailsFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { is: 'NULL' }, + } satisfies EmailsFilter, + }, + ], + }; + } + case 'additionalEmails': { + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { is: 'NULL' }, + } satisfies EmailsFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { like: '[]' }, + } satisfies EmailsFilter, + }, + ], + }; + } + default: { + throw new Error(`Unknown subfield name ${subFieldName}`); + } + } + } + + return { + and: [ + { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { eq: '' }, + } satisfies EmailsFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { is: 'NULL' }, + } satisfies EmailsFilter, + }, + ], + }, + { + or: [ + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { is: 'NULL' }, + } satisfies EmailsFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { like: '[]' }, + } satisfies EmailsFilter, + }, + ], + }, + ], + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks.ts new file mode 100644 index 000000000..f9431559e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks.ts @@ -0,0 +1,122 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { + LinksFilter, + RecordGqlOperationFilter, +} from '@/object-record/graphql/types/RecordGqlOperationFilter'; + +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isNonEmptyString } from '@sniptt/guards'; + +export const computeEmptyGqlOperationFilterForLinks = ({ + recordFilter, + correspondingFieldMetadataItem, +}: { + recordFilter: RecordFilter; + correspondingFieldMetadataItem: Pick; +}): RecordGqlOperationFilter => { + const subFieldName = recordFilter.subFieldName; + const isSubFieldFilter = isNonEmptyString(subFieldName); + + if (isSubFieldFilter) { + switch (subFieldName) { + case 'primaryLinkLabel': { + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { eq: '' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { is: 'NULL' }, + } satisfies LinksFilter, + }, + ], + }; + } + case 'primaryLinkUrl': { + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { eq: '' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { is: 'NULL' }, + } satisfies LinksFilter, + }, + ], + }; + } + case 'secondaryLinks': { + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { is: 'NULL' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { like: '[]' }, + } satisfies LinksFilter, + }, + ], + }; + } + default: { + throw new Error(`Unknown subfield name ${subFieldName}`); + } + } + } + + return { + and: [ + { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { eq: '' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { is: 'NULL' }, + } satisfies LinksFilter, + }, + ], + }, + { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { eq: '' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { is: 'NULL' }, + } satisfies LinksFilter, + }, + ], + }, + { + or: [ + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { is: 'NULL' }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { like: '[]' }, + } satisfies LinksFilter, + }, + ], + }, + ], + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldComputeEmptinessFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldComputeEmptinessFilter.ts new file mode 100644 index 000000000..0d06a990c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldComputeEmptinessFilter.ts @@ -0,0 +1,31 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand'; + +export const checkIfShouldComputeEmptinessFilter = ({ + recordFilter, + correspondingFieldMetadataItem, +}: { + recordFilter: RecordFilter; + correspondingFieldMetadataItem: Pick; +}) => { + const isAnEmptinessOperand = isEmptinessOperand(recordFilter.operand); + + const filterTypesThatHaveNoEmptinessOperand: FilterableFieldType[] = [ + 'BOOLEAN', + ]; + + const filterType = getFilterTypeFromFieldType( + correspondingFieldMetadataItem.type, + ); + + const filterHasEmptinessOperands = + !filterTypesThatHaveNoEmptinessOperand.includes(filterType); + + const shouldComputeEmptinessFilter = + filterHasEmptinessOperands && isAnEmptinessOperand; + + return shouldComputeEmptinessFilter; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering.ts new file mode 100644 index 000000000..deb7dfc82 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering.ts @@ -0,0 +1,26 @@ +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand'; +import { isDefined } from 'twenty-shared/utils'; + +export const checkIfShouldSkipFiltering = ({ + recordFilter, +}: { + recordFilter: RecordFilter; +}) => { + const isAnEmptinessOperand = isEmptinessOperand(recordFilter.operand); + + const isDateOperandWithoutValue = [ + RecordFilterOperand.IsInPast, + RecordFilterOperand.IsInFuture, + RecordFilterOperand.IsToday, + ].includes(recordFilter.operand); + + const isFilterValueEmpty = + !isDefined(recordFilter.value) || recordFilter.value === ''; + + const shouldSkipFiltering = + !isAnEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty; + + return shouldSkipFiltering; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts new file mode 100644 index 000000000..352591fe5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts @@ -0,0 +1,153 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { + EmailsFilter, + RecordGqlOperationFilter, +} from '@/object-record/graphql/types/RecordGqlOperationFilter'; + +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { isNonEmptyString } from '@sniptt/guards'; + +export const computeGqlOperationFilterForEmails = ({ + recordFilter, + correspondingFieldMetadataItem, + subFieldName, +}: { + recordFilter: RecordFilter; + correspondingFieldMetadataItem: Pick; + subFieldName: CompositeFieldSubFieldName | null | undefined; +}): RecordGqlOperationFilter => { + const isSubFieldFilter = isNonEmptyString(subFieldName); + + if (isSubFieldFilter) { + switch (subFieldName) { + case 'primaryEmail': { + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }; + case RecordFilterOperand.DoesNotContain: + return { + not: { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } + } + case 'additionalEmails': { + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + like: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }; + case RecordFilterOperand.DoesNotContain: + return { + or: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + like: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + }, + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + is: 'NULL', + }, + } satisfies EmailsFilter, + }, + ], + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } + } + default: { + throw new Error(`Unknown subfield name ${subFieldName}`); + } + } + } + + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + like: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + ], + }; + case RecordFilterOperand.DoesNotContain: + return { + and: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + primaryEmail: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + }, + { + or: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + like: `%${recordFilter.value}%`, + }, + } satisfies EmailsFilter, + }, + }, + { + [correspondingFieldMetadataItem.name]: { + additionalEmails: { + is: 'NULL', + }, + } satisfies EmailsFilter, + }, + ], + }, + ], + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts new file mode 100644 index 000000000..a1a55570e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts @@ -0,0 +1,166 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { LinksFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { isNonEmptyString } from '@sniptt/guards'; + +export const computeGqlOperationFilterForLinks = ({ + recordFilter, + correspondingFieldMetadataItem, + subFieldName, +}: { + recordFilter: RecordFilter; + correspondingFieldMetadataItem: Pick; + subFieldName: CompositeFieldSubFieldName | null | undefined; +}) => { + const isSubFieldFilter = isNonEmptyString(subFieldName); + + if (isSubFieldFilter) { + switch (subFieldName) { + case 'primaryLinkLabel': + case 'primaryLinkUrl': { + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + [correspondingFieldMetadataItem.name]: { + [subFieldName]: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }; + case RecordFilterOperand.DoesNotContain: + return { + not: { + [correspondingFieldMetadataItem.name]: { + [subFieldName]: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } + } + case 'secondaryLinks': { + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + like: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }; + case RecordFilterOperand.DoesNotContain: + return { + or: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + like: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + }, + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + is: 'NULL', + }, + } satisfies LinksFilter, + }, + ], + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } + } + default: { + throw new Error(`Unknown subfield name ${subFieldName}`); + } + } + } + + switch (recordFilter.operand) { + case RecordFilterOperand.Contains: + return { + or: [ + { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + like: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + ], + }; + case RecordFilterOperand.DoesNotContain: + return { + and: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + primaryLinkLabel: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + }, + { + not: { + [correspondingFieldMetadataItem.name]: { + primaryLinkUrl: { + ilike: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + }, + { + or: [ + { + not: { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + like: `%${recordFilter.value}%`, + }, + } satisfies LinksFilter, + }, + }, + { + [correspondingFieldMetadataItem.name]: { + secondaryLinks: { + is: 'NULL', + }, + } satisfies LinksFilter, + }, + ], + }, + ], + }; + default: + throw new Error( + `Unknown operand ${recordFilter.operand} for ${correspondingFieldMetadataItem.type} filter`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterGroupIntoGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterGroupIntoGqlOperationFilter.ts new file mode 100644 index 000000000..b6ed1951c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterGroupIntoGqlOperationFilter.ts @@ -0,0 +1,83 @@ +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { Field } from '~/generated/graphql'; + +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; + +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; +import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter'; +import { isDefined } from 'twenty-shared/utils'; + +export const turnRecordFilterGroupsIntoGqlOperationFilter = ( + filterValueDependencies: RecordFilterValueDependencies, + filters: RecordFilter[], + fields: Pick[], + recordFilterGroups: RecordFilterGroup[], + currentRecordFilterGroupId?: string, +): RecordGqlOperationFilter | undefined => { + const currentRecordFilterGroup = recordFilterGroups.find( + (recordFilterGroup) => recordFilterGroup.id === currentRecordFilterGroupId, + ); + + if (!isDefined(currentRecordFilterGroup)) { + return; + } + + const recordFiltersInGroup = filters.filter( + (filter) => filter.recordFilterGroupId === currentRecordFilterGroupId, + ); + + const groupRecordGqlOperationFilters = recordFiltersInGroup + .map((recordFilter) => + turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: recordFilter, + fieldMetadataItems: fields, + }), + ) + .filter(isDefined); + + const subGroupRecordGqlOperationFilters = recordFilterGroups + .filter( + (recordFilterGroup) => + recordFilterGroup.parentRecordFilterGroupId === + currentRecordFilterGroupId, + ) + .map((subRecordFilterGroup) => + turnRecordFilterGroupsIntoGqlOperationFilter( + filterValueDependencies, + filters, + fields, + recordFilterGroups, + subRecordFilterGroup.id, + ), + ) + .filter(isDefined); + + if ( + currentRecordFilterGroup.logicalOperator === + RecordFilterGroupLogicalOperator.AND + ) { + return { + and: [ + ...groupRecordGqlOperationFilters, + ...subGroupRecordGqlOperationFilters, + ], + }; + } else if ( + currentRecordFilterGroup.logicalOperator === + RecordFilterGroupLogicalOperator.OR + ) { + return { + or: [ + ...groupRecordGqlOperationFilters, + ...subGroupRecordGqlOperationFilters, + ], + }; + } else { + throw new Error( + `Unknown logical operator ${currentRecordFilterGroup.logicalOperator}`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts similarity index 60% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts index a529780c3..3308bb54c 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -7,7 +7,6 @@ import { BooleanFilter, CurrencyFilter, DateFilter, - EmailsFilter, FloatFilter, MultiSelectFilter, PhonesFilter, @@ -40,135 +39,124 @@ import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; -import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; -import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; -import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; -import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand'; +import { checkIfShouldComputeEmptinessFilter } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldComputeEmptinessFilter'; +import { checkIfShouldSkipFiltering } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/checkIfShouldSkipFiltering'; +import { computeGqlOperationFilterForEmails } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails'; +import { computeGqlOperationFilterForLinks } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -type ComputeFilterRecordGqlOperationFilterParams = { +type TurnRecordFilterIntoRecordGqlOperationFilterParams = { filterValueDependencies: RecordFilterValueDependencies; - filter: RecordFilter; + recordFilter: RecordFilter; fieldMetadataItems: Pick[]; }; -export const computeFilterRecordGqlOperationFilter = ({ +export const turnRecordFilterIntoRecordGqlOperationFilter = ({ filterValueDependencies, - filter, - fieldMetadataItems: fields, -}: ComputeFilterRecordGqlOperationFilterParams): + recordFilter, + fieldMetadataItems, +}: TurnRecordFilterIntoRecordGqlOperationFilterParams): | RecordGqlOperationFilter | undefined => { - const correspondingField = fields.find( - (field) => field.id === filter.fieldMetadataId, + const correspondingFieldMetadataItem = fieldMetadataItems.find( + (field) => field.id === recordFilter.fieldMetadataId, ); - const subFieldName = filter.subFieldName; - - const isSubFieldFilter = isNonEmptyString(subFieldName); - - const isAnEmptinessOperand = isEmptinessOperand(filter.operand); - - const isDateOperandWithoutValue = [ - RecordFilterOperand.IsInPast, - RecordFilterOperand.IsInFuture, - RecordFilterOperand.IsToday, - ].includes(filter.operand); - - if (!correspondingField) { + if (!isDefined(correspondingFieldMetadataItem)) { return; } - const filterType = getFilterTypeFromFieldType(correspondingField.type); - - const isFilterValueEmpty = !isDefined(filter.value) || filter.value === ''; - - const shouldSkipFiltering = - !isAnEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty; + const shouldSkipFiltering = checkIfShouldSkipFiltering({ recordFilter }); if (shouldSkipFiltering) { return; } - const filterTypesThatHaveNoEmptinessOperand: FilterableFieldType[] = [ - 'BOOLEAN', - ]; + const shouldComputeEmptinessFilter = checkIfShouldComputeEmptinessFilter({ + recordFilter, + correspondingFieldMetadataItem, + }); - const filterHasEmptinessOperands = - !filterTypesThatHaveNoEmptinessOperand.includes(filterType); - - if (filterHasEmptinessOperands && isAnEmptinessOperand) { - const emptyOperationFilter = getEmptyRecordGqlOperationFilter({ - operand: filter.operand, - correspondingField, - recordFilter: filter, + if (shouldComputeEmptinessFilter) { + const emptinessFilter = getEmptyRecordGqlOperationFilter({ + operand: recordFilter.operand, + correspondingField: correspondingFieldMetadataItem, + recordFilter: recordFilter, }); - return emptyOperationFilter; + return emptinessFilter; } + const subFieldName = recordFilter.subFieldName; + + const isSubFieldFilter = isNonEmptyString(subFieldName); + + const filterType = getFilterTypeFromFieldType( + correspondingFieldMetadataItem.type, + ); + switch (filterType) { case 'TEXT': - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { - [correspondingField.name]: { - ilike: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + ilike: `%${recordFilter.value}%`, } as StringFilter, }; case RecordFilterOperand.DoesNotContain: return { not: { - [correspondingField.name]: { - ilike: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + ilike: `%${recordFilter.value}%`, } as StringFilter, }, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } case 'RAW_JSON': - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { - [correspondingField.name]: { - like: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + like: `%${recordFilter.value}%`, } as RawJsonFilter, }; case RecordFilterOperand.DoesNotContain: return { not: { - [correspondingField.name]: { - like: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + like: `%${recordFilter.value}%`, } as RawJsonFilter, }, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } case 'DATE': case 'DATE_TIME': { - const resolvedFilterValue = resolveDateViewFilterValue(filter); + const resolvedFilterValue = resolveDateViewFilterValue(recordFilter); const now = roundToNearestMinutes(new Date()); const date = resolvedFilterValue instanceof Date ? resolvedFilterValue : now; - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.IsAfter: { return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { gt: date.toISOString(), } as DateFilter, }; } case RecordFilterOperand.IsBefore: { return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { lt: date.toISOString(), } as DateFilter, }; @@ -192,12 +180,12 @@ export const computeFilterRecordGqlOperationFilter = ({ return { and: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { gte: start.toISOString(), } as DateFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { lte: end.toISOString(), } as DateFilter, }, @@ -211,12 +199,12 @@ export const computeFilterRecordGqlOperationFilter = ({ return { and: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { lte: endOfDay(date).toISOString(), } as DateFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { gte: startOfDay(date).toISOString(), } as DateFilter, }, @@ -225,13 +213,13 @@ export const computeFilterRecordGqlOperationFilter = ({ } case RecordFilterOperand.IsInPast: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { lte: now.toISOString(), } as DateFilter, }; case RecordFilterOperand.IsInFuture: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { gte: now.toISOString(), } as DateFilter, }; @@ -239,12 +227,12 @@ export const computeFilterRecordGqlOperationFilter = ({ return { and: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { lte: endOfDay(now).toISOString(), } as DateFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { gte: startOfDay(now).toISOString(), } as DateFilter, }, @@ -253,56 +241,56 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, // + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, // ); } } case 'RATING': - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Is: return { - [correspondingField.name]: { - eq: convertRatingToRatingValue(parseFloat(filter.value)), + [correspondingFieldMetadataItem.name]: { + eq: convertRatingToRatingValue(parseFloat(recordFilter.value)), } as RatingFilter, }; case RecordFilterOperand.GreaterThan: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { in: convertGreaterThanRatingToArrayOfRatingValues( - parseFloat(filter.value), + parseFloat(recordFilter.value), ), } as RatingFilter, }; case RecordFilterOperand.LessThan: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { in: convertLessThanRatingToArrayOfRatingValues( - parseFloat(filter.value), + parseFloat(recordFilter.value), ), } as RatingFilter, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } case 'NUMBER': - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.GreaterThan: return { - [correspondingField.name]: { - gte: parseFloat(filter.value), + [correspondingFieldMetadataItem.name]: { + gte: parseFloat(recordFilter.value), } as FloatFilter, }; case RecordFilterOperand.LessThan: return { - [correspondingField.name]: { - lte: parseFloat(filter.value), + [correspondingFieldMetadataItem.name]: { + lte: parseFloat(recordFilter.value), } as FloatFilter, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } case 'RELATION': { @@ -311,10 +299,10 @@ export const computeFilterRecordGqlOperationFilter = ({ .catch({ isCurrentWorkspaceMemberSelected: false, selectedRecordIds: simpleRelationFilterValueSchema.parse( - filter.value, + recordFilter.value, ), }) - .parse(filter.value); + .parse(recordFilter.value); const recordIds = isCurrentWorkspaceMemberSelected ? [ @@ -325,10 +313,10 @@ export const computeFilterRecordGqlOperationFilter = ({ if (recordIds.length === 0) return; - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Is: return { - [correspondingField.name + 'Id']: { + [correspondingFieldMetadataItem.name + 'Id']: { in: recordIds, } as RelationFilter, }; @@ -338,13 +326,13 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name + 'Id']: { + [correspondingFieldMetadataItem.name + 'Id']: { in: recordIds, } as RelationFilter, }, }, { - [correspondingField.name + 'Id']: { + [correspondingFieldMetadataItem.name + 'Id']: { is: 'NULL', } as RelationFilter, }, @@ -353,7 +341,7 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } @@ -365,17 +353,17 @@ export const computeFilterRecordGqlOperationFilter = ({ subFieldName, ) ) { - const parsedCurrencyCodes = JSON.parse(filter.value) as string[]; + const parsedCurrencyCodes = JSON.parse(recordFilter.value) as string[]; if (parsedCurrencyCodes.length === 0) return undefined; const gqlFilter: RecordGqlOperationFilter = { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { currencyCode: { in: parsedCurrencyCodes }, } as CurrencyFilter, }; - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Is: return gqlFilter; case RecordFilterOperand.IsNot: @@ -384,7 +372,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} / ${subFieldName} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} / ${subFieldName} filter`, ); } } else if ( @@ -395,36 +383,38 @@ export const computeFilterRecordGqlOperationFilter = ({ ) || !isSubFieldFilter ) { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.GreaterThan: return { - [correspondingField.name]: { - amountMicros: { gte: parseFloat(filter.value) * 1000000 }, + [correspondingFieldMetadataItem.name]: { + amountMicros: { gte: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.LessThan: return { - [correspondingField.name]: { - amountMicros: { lte: parseFloat(filter.value) * 1000000 }, + [correspondingFieldMetadataItem.name]: { + amountMicros: { lte: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.Is: return { - [correspondingField.name]: { - amountMicros: { eq: parseFloat(filter.value) * 1000000 }, + [correspondingFieldMetadataItem.name]: { + amountMicros: { eq: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.IsNot: return { not: { - [correspondingField.name]: { - amountMicros: { eq: parseFloat(filter.value) * 1000000 }, + [correspondingFieldMetadataItem.name]: { + amountMicros: { + eq: parseFloat(recordFilter.value) * 1000000, + }, } as CurrencyFilter, }, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} / ${subFieldName} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} / ${subFieldName} filter`, ); } } else { @@ -433,62 +423,20 @@ export const computeFilterRecordGqlOperationFilter = ({ ); } } - case 'LINKS': { - const linksFilters = generateILikeFiltersForCompositeFields( - filter.value, - correspondingField.name, - ['primaryLinkLabel', 'primaryLinkUrl'], - ); - - switch (filter.operand) { - case RecordFilterOperand.Contains: - if (!isSubFieldFilter) { - return { - or: linksFilters, - }; - } else { - return { - [correspondingField.name]: { - [subFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }; - } - case RecordFilterOperand.DoesNotContain: - if (!isSubFieldFilter) { - return { - and: linksFilters.map((filter) => { - return { - not: filter, - }; - }), - }; - } else { - return { - not: { - [correspondingField.name]: { - [subFieldName]: { - ilike: `%${filter.value}%`, - }, - }, - }, - }; - } - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, - ); - } + return computeGqlOperationFilterForLinks({ + correspondingFieldMetadataItem, + recordFilter, + subFieldName, + }); } case 'FULL_NAME': { const fullNameFilters = generateILikeFiltersForCompositeFields( - filter.value, - correspondingField.name, + recordFilter.value, + correspondingFieldMetadataItem.name, ['firstName', 'lastName'], ); - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: if (!isSubFieldFilter) { return { @@ -496,9 +444,9 @@ export const computeFilterRecordGqlOperationFilter = ({ }; } else { return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, }, }; @@ -515,9 +463,9 @@ export const computeFilterRecordGqlOperationFilter = ({ } else { return { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, }, }, @@ -525,55 +473,55 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } case 'ADDRESS': - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: if (!isSubFieldFilter) { return { or: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet1: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet2: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCity: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressState: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCountry: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressPostcode: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, @@ -581,14 +529,19 @@ export const computeFilterRecordGqlOperationFilter = ({ }; } else { if (subFieldName === 'addressCountry') { - const parsedCountryCodes = JSON.parse(filter.value) as string[]; + const parsedCountryCodes = JSON.parse( + recordFilter.value, + ) as string[]; - if (filter.value === '[]' || parsedCountryCodes.length === 0) { + if ( + recordFilter.value === '[]' || + parsedCountryCodes.length === 0 + ) { return {}; } return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { in: parsedCountryCodes, } as AddressFilter, @@ -597,9 +550,9 @@ export const computeFilterRecordGqlOperationFilter = ({ } return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, } as AddressFilter, }, }; @@ -612,15 +565,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet1: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet1: { is: 'NULL', }, @@ -632,15 +585,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet2: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressStreet2: { is: 'NULL', }, @@ -652,15 +605,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCity: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCity: { is: 'NULL', }, @@ -672,15 +625,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressState: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressState: { is: 'NULL', }, @@ -692,15 +645,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressPostcode: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressPostcode: { is: 'NULL', }, @@ -712,15 +665,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCountry: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCountry: { is: 'NULL', }, @@ -732,9 +685,14 @@ export const computeFilterRecordGqlOperationFilter = ({ }; } else { if (subFieldName === 'addressCountry') { - const parsedCountryCodes = JSON.parse(filter.value) as string[]; + const parsedCountryCodes = JSON.parse( + recordFilter.value, + ) as string[]; - if (filter.value === '[]' || parsedCountryCodes.length === 0) { + if ( + recordFilter.value === '[]' || + parsedCountryCodes.length === 0 + ) { return {}; } @@ -742,15 +700,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCountry: { - in: JSON.parse(filter.value), + in: JSON.parse(recordFilter.value), } as AddressFilter, }, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { addressCountry: { is: 'NULL', } as AddressFilter, @@ -764,15 +722,15 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, } as AddressFilter, }, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { [subFieldName]: { is: 'NULL', } as AddressFilter, @@ -783,24 +741,24 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } case 'MULTI_SELECT': { - const options = resolveSelectViewFilterValue(filter); + const options = resolveSelectViewFilterValue(recordFilter); if (options.length === 0) return; const emptyOptions = options.filter((option: string) => option === ''); const nonEmptyOptions = options.filter((option: string) => option !== ''); - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: { const conditions = []; if (nonEmptyOptions.length > 0) { conditions.push({ - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { containsAny: nonEmptyOptions, } as MultiSelectFilter, }); @@ -808,7 +766,7 @@ export const computeFilterRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { isEmptyArray: true, } as MultiSelectFilter, }); @@ -821,18 +779,18 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { containsAny: nonEmptyOptions, } as MultiSelectFilter, }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { isEmptyArray: true, } as MultiSelectFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { is: 'NULL', } as MultiSelectFilter, }, @@ -840,25 +798,25 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } case 'SELECT': { - const options = resolveSelectViewFilterValue(filter); + const options = resolveSelectViewFilterValue(recordFilter); if (options.length === 0) return; const emptyOptions = options.filter((option: string) => option === ''); const nonEmptyOptions = options.filter((option: string) => option !== ''); - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Is: { const conditions = []; if (nonEmptyOptions.length > 0) { conditions.push({ - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { in: nonEmptyOptions, } as SelectFilter, }); @@ -866,7 +824,7 @@ export const computeFilterRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { is: 'NULL', } as SelectFilter, }); @@ -880,7 +838,7 @@ export const computeFilterRecordGqlOperationFilter = ({ if (nonEmptyOptions.length > 0) { conditions.push({ not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { in: nonEmptyOptions, } as SelectFilter, }, @@ -890,7 +848,7 @@ export const computeFilterRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { is: 'NULL', } as SelectFilter, }, @@ -901,29 +859,29 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } case 'ARRAY': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { - [correspondingField.name]: { - containsIlike: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + containsIlike: `%${recordFilter.value}%`, } as ArrayFilter, }; case RecordFilterOperand.DoesNotContain: return { not: { - [correspondingField.name]: { - containsIlike: `%${filter.value}%`, + [correspondingFieldMetadataItem.name]: { + containsIlike: `%${recordFilter.value}%`, } as ArrayFilter, }, }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } @@ -931,16 +889,18 @@ export const computeFilterRecordGqlOperationFilter = ({ if (isSubFieldFilter) { switch (subFieldName) { case 'source': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Is: { - if (filter.value === '[]') { + if (recordFilter.value === '[]') { return; } - const parsedSources = JSON.parse(filter.value) as string[]; + const parsedSources = JSON.parse( + recordFilter.value, + ) as string[]; return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { source: { in: parsedSources, } satisfies RelationFilter, @@ -948,17 +908,19 @@ export const computeFilterRecordGqlOperationFilter = ({ }; } case RecordFilterOperand.IsNot: { - if (filter.value === '[]') { + if (recordFilter.value === '[]') { return; } - const parsedSources = JSON.parse(filter.value) as string[]; + const parsedSources = JSON.parse( + recordFilter.value, + ) as string[]; if (parsedSources.length === 0) return; return { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { source: { in: parsedSources, } satisfies RelationFilter, @@ -968,19 +930,19 @@ export const computeFilterRecordGqlOperationFilter = ({ } default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.label} filter`, + `Unknown operand ${recordFilter.operand} for ${recordFilter.label} filter`, ); } } case 'name': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { or: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { name: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } satisfies ActorFilter, }, @@ -991,9 +953,9 @@ export const computeFilterRecordGqlOperationFilter = ({ and: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { name: { - ilike: `%${filter.value}%`, + ilike: `%${recordFilter.value}%`, }, } satisfies ActorFilter, }, @@ -1002,42 +964,38 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.label} filter`, + `Unknown operand ${recordFilter.operand} for ${recordFilter.label} filter`, ); } } } break; } else { - if (filter.value === '[]') { + if (recordFilter.value === '[]') { return; } - const parsedSources = JSON.parse(filter.value) as string[]; + const parsedSources = JSON.parse(recordFilter.value) as string[]; if (parsedSources.length === 0) return; - switch (filter.operand) { - case RecordFilterOperand.Contains: + switch (recordFilter.operand) { + case RecordFilterOperand.Is: return { - or: [ - { - [correspondingField.name]: { - source: { - in: parsedSources, - }, - } satisfies ActorFilter, + [correspondingFieldMetadataItem.name]: { + source: { + in: parsedSources, }, - ], + } satisfies ActorFilter, }; - case RecordFilterOperand.DoesNotContain: + case RecordFilterOperand.IsNot: return { and: [ { or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { source: { in: parsedSources, }, @@ -1045,7 +1003,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { source: { is: 'NULL', }, @@ -1057,72 +1015,46 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.label} filter`, + `Unknown operand ${recordFilter.operand} for ${recordFilter.label} filter`, ); } } } - case 'EMAILS': - switch (filter.operand) { - case RecordFilterOperand.Contains: - return { - or: [ - { - [correspondingField.name]: { - primaryEmail: { - ilike: `%${filter.value}%`, - }, - } as EmailsFilter, - }, - ], - }; - case RecordFilterOperand.DoesNotContain: - return { - and: [ - { - not: { - [correspondingField.name]: { - primaryEmail: { - ilike: `%${filter.value}%`, - }, - } as EmailsFilter, - }, - }, - ], - }; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, - ); - } + case 'EMAILS': { + return computeGqlOperationFilterForEmails({ + correspondingFieldMetadataItem, + recordFilter, + subFieldName, + }); + } case 'PHONES': { if (!isSubFieldFilter) { - const filterValue = filter.value.replace(/[^0-9]/g, ''); + const filterValue = recordFilter.value.replace(/[^0-9]/g, ''); if (!isNonEmptyString(filterValue)) { return; } - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { or: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, } as PhonesFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, } as PhonesFilter, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1135,7 +1067,7 @@ export const computeFilterRecordGqlOperationFilter = ({ and: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1144,7 +1076,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }, { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1155,7 +1087,7 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1163,7 +1095,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { is: 'NULL', } as PhonesFilter, @@ -1175,21 +1107,21 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } - const filterValue = filter.value; + const filterValue = recordFilter.value; switch (subFieldName) { case 'additionalPhones': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { or: [ { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1202,7 +1134,7 @@ export const computeFilterRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1210,7 +1142,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }, }, { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { additionalPhones: { is: 'NULL', } as PhonesFilter, @@ -1220,15 +1152,15 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } case 'primaryPhoneNumber': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1237,7 +1169,7 @@ export const computeFilterRecordGqlOperationFilter = ({ case RecordFilterOperand.DoesNotContain: return { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1246,15 +1178,15 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } case 'primaryPhoneCallingCode': { - switch (filter.operand) { + switch (recordFilter.operand) { case RecordFilterOperand.Contains: return { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1263,7 +1195,7 @@ export const computeFilterRecordGqlOperationFilter = ({ case RecordFilterOperand.DoesNotContain: return { not: { - [correspondingField.name]: { + [correspondingFieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1272,7 +1204,7 @@ export const computeFilterRecordGqlOperationFilter = ({ }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, + `Unknown operand ${recordFilter.operand} for ${filterType} filter`, ); } } @@ -1284,8 +1216,8 @@ export const computeFilterRecordGqlOperationFilter = ({ } case 'BOOLEAN': { return { - [correspondingField.name]: { - eq: filter.value === 'true', + [correspondingFieldMetadataItem.name]: { + eq: recordFilter.value === 'true', } as BooleanFilter, }; } @@ -1293,132 +1225,3 @@ export const computeFilterRecordGqlOperationFilter = ({ throw new Error('Unknown filter type'); } }; - -const computeRecordFilterGroupRecordGqlOperationFilter = ( - filterValueDependencies: RecordFilterValueDependencies, - filters: RecordFilter[], - fields: Pick[], - recordFilterGroups: RecordFilterGroup[], - currentRecordFilterGroupId?: string, -): RecordGqlOperationFilter | undefined => { - const currentRecordFilterGroup = recordFilterGroups.find( - (recordFilterGroup) => recordFilterGroup.id === currentRecordFilterGroupId, - ); - - if (!currentRecordFilterGroup) { - return; - } - - const recordFiltersInGroup = filters.filter( - (filter) => filter.recordFilterGroupId === currentRecordFilterGroupId, - ); - - const groupRecordGqlOperationFilters = recordFiltersInGroup - .map((recordFilter) => - computeFilterRecordGqlOperationFilter({ - filterValueDependencies, - filter: recordFilter, - fieldMetadataItems: fields, - }), - ) - .filter(isDefined); - - const subGroupRecordGqlOperationFilters = recordFilterGroups - .filter( - (recordFilterGroup) => - recordFilterGroup.parentRecordFilterGroupId === - currentRecordFilterGroupId, - ) - .map((subViewFilterGroup) => - computeRecordFilterGroupRecordGqlOperationFilter( - filterValueDependencies, - filters, - fields, - recordFilterGroups, - subViewFilterGroup.id, - ), - ) - .filter(isDefined); - - if ( - currentRecordFilterGroup.logicalOperator === - RecordFilterGroupLogicalOperator.AND - ) { - return { - and: [ - ...groupRecordGqlOperationFilters, - ...subGroupRecordGqlOperationFilters, - ], - }; - } else if ( - currentRecordFilterGroup.logicalOperator === - RecordFilterGroupLogicalOperator.OR - ) { - return { - or: [ - ...groupRecordGqlOperationFilters, - ...subGroupRecordGqlOperationFilters, - ], - }; - } else { - throw new Error( - `Unknown logical operator ${currentRecordFilterGroup.logicalOperator}`, - ); - } -}; - -export const computeRecordGqlOperationFilter = ({ - fields, - filterValueDependencies, - recordFilters, - recordFilterGroups, -}: { - filterValueDependencies: RecordFilterValueDependencies; - recordFilters: RecordFilter[]; - fields: Pick[]; - recordFilterGroups: RecordFilterGroup[]; -}): RecordGqlOperationFilter => { - const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = - recordFilters - .filter((filter) => !isDefined(filter.recordFilterGroupId)) - .map((regularFilter) => - computeFilterRecordGqlOperationFilter({ - filterValueDependencies, - filter: regularFilter, - fieldMetadataItems: fields, - }), - ) - .filter(isDefined); - - const outermostFilterGroupId = recordFilterGroups.find( - (recordFilterGroup) => !recordFilterGroup.parentRecordFilterGroupId, - )?.id; - - const advancedRecordGqlOperationFilter = - computeRecordFilterGroupRecordGqlOperationFilter( - filterValueDependencies, - recordFilters, - fields, - recordFilterGroups, - outermostFilterGroupId, - ); - - const recordGqlOperationFilters = [ - ...regularRecordGqlOperationFilter, - advancedRecordGqlOperationFilter, - ].filter(isDefined); - - if (recordGqlOperationFilters.length === 0) { - return {}; - } - - if (recordGqlOperationFilters.length === 1) { - return recordGqlOperationFilters[0]; - } - - const recordGqlOperationFilter = { - and: recordGqlOperationFilters, - }; - - return recordGqlOperationFilter; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeRecordGqlOperationFilter.ts new file mode 100644 index 000000000..98e6811d1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeRecordGqlOperationFilter.ts @@ -0,0 +1,66 @@ +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { Field } from '~/generated/graphql'; + +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; + +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { turnRecordFilterGroupsIntoGqlOperationFilter } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterGroupIntoGqlOperationFilter'; +import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter'; +import { isDefined } from 'twenty-shared/utils'; + +export const computeRecordGqlOperationFilter = ({ + fields, + filterValueDependencies, + recordFilters, + recordFilterGroups, +}: { + filterValueDependencies: RecordFilterValueDependencies; + recordFilters: RecordFilter[]; + fields: Pick[]; + recordFilterGroups: RecordFilterGroup[]; +}): RecordGqlOperationFilter => { + const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = + recordFilters + .filter((filter) => !isDefined(filter.recordFilterGroupId)) + .map((regularFilter) => + turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: regularFilter, + fieldMetadataItems: fields, + }), + ) + .filter(isDefined); + + const outermostFilterGroupId = recordFilterGroups.find( + (recordFilterGroup) => !recordFilterGroup.parentRecordFilterGroupId, + )?.id; + + const advancedRecordGqlOperationFilter = + turnRecordFilterGroupsIntoGqlOperationFilter( + filterValueDependencies, + recordFilters, + fields, + recordFilterGroups, + outermostFilterGroupId, + ); + + const recordGqlOperationFilters = [ + ...regularRecordGqlOperationFilter, + advancedRecordGqlOperationFilter, + ].filter(isDefined); + + if (recordGqlOperationFilters.length === 0) { + return {}; + } + + if (recordGqlOperationFilters.length === 1) { + return recordGqlOperationFilters[0]; + } + + const recordGqlOperationFilter = { + and: recordGqlOperationFilters, + }; + + return recordGqlOperationFilter; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts index 51fb37acc..589788ff2 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts @@ -5,7 +5,6 @@ import { ArrayFilter, CurrencyFilter, DateFilter, - EmailsFilter, FloatFilter, MultiSelectFilter, PhonesFilter, @@ -15,9 +14,10 @@ import { RelationFilter, SelectFilter, StringFilter, - URLFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { computeEmptyGqlOperationFilterForEmails } from '@/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails'; +import { computeEmptyGqlOperationFilterForLinks } from '@/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isNonEmptyString } from '@sniptt/guards'; import { Field } from '~/generated/graphql'; @@ -187,33 +187,10 @@ export const getEmptyRecordGqlOperationFilter = ({ break; } case 'LINKS': { - if (!isSubFieldFilter) { - const linksFilters = generateILikeFiltersForCompositeFields( - '', - correspondingField.name, - ['primaryLinkLabel', 'primaryLinkUrl'], - true, - ); - - emptyRecordFilter = { - and: linksFilters, - }; - } else { - emptyRecordFilter = { - or: [ - { - [correspondingField.name]: { - [compositeFieldName]: { ilike: '' }, - } as URLFilter, - }, - { - [correspondingField.name]: { - [compositeFieldName]: { is: 'NULL' }, - } as URLFilter, - }, - ], - }; - } + emptyRecordFilter = computeEmptyGqlOperationFilterForLinks({ + correspondingFieldMetadataItem: correspondingField, + recordFilter, + }); break; } case 'ADDRESS': @@ -401,20 +378,10 @@ export const getEmptyRecordGqlOperationFilter = ({ }; break; case 'EMAILS': - emptyRecordFilter = { - or: [ - { - [correspondingField.name]: { - primaryEmail: { ilike: '' }, - } as EmailsFilter, - }, - { - [correspondingField.name]: { - primaryEmail: { is: 'NULL' }, - } as EmailsFilter, - }, - ], - }; + emptyRecordFilter = computeEmptyGqlOperationFilterForEmails({ + correspondingFieldMetadataItem: correspondingField, + recordFilter, + }); break; default: throw new Error(`Unsupported empty filter type ${filterType}`); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts index 828e68462..53a66fce3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts @@ -3,6 +3,7 @@ import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dro import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand'; +import { isNonEmptyString } from '@sniptt/guards'; import { FieldMetadataType } from 'twenty-shared/types'; import { assertUnreachable } from 'twenty-shared/utils'; @@ -147,6 +148,8 @@ export const getRecordFilterOperands = ({ filterType, subFieldName, }: GetRecordFilterOperandsParams) => { + const isFilterOnSubField = isNonEmptyString(subFieldName); + switch (filterType) { case 'TEXT': case 'EMAILS': @@ -184,7 +187,7 @@ export const getRecordFilterOperands = ({ case 'SELECT': return FILTER_OPERANDS_MAP.SELECT; case 'ACTOR': { - if (isFilterOnActorSourceSubField(subFieldName)) { + if (isFilterOnActorSourceSubField(subFieldName) || !isFilterOnSubField) { return [ RecordFilterOperand.Is, RecordFilterOperand.IsNot, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index 853d20971..c8a88ee13 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -3,7 +3,7 @@ import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 6e99e6dc5..a601f3979 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -8,7 +8,7 @@ import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/use import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts index 3d3f588f8..7d35ae0c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts @@ -5,7 +5,7 @@ import { computeAggregateValueAndLabel } from '@/object-record/record-board/reco import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { UserContext } from '@/users/contexts/UserContext'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index 66d8a4802..e55c07fdf 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -3,7 +3,7 @@ import { computeAggregateValueAndLabel } from '@/object-record/record-board/reco import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index ef8bd887d..10c15e8de 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -56,7 +56,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { label: 'Emails', Icon: IllustrationIconMail, subFields: ['primaryEmail', 'additionalEmails'], - filterableSubFields: ['primaryEmail'], + filterableSubFields: ['primaryEmail', 'additionalEmails'], labelBySubField: { primaryEmail: 'Primary Email', additionalEmails: 'Additional Emails', @@ -81,7 +81,11 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { }, category: 'Basic', subFields: ['primaryLinkUrl', 'primaryLinkLabel', 'secondaryLinks'], - filterableSubFields: ['primaryLinkUrl', 'primaryLinkLabel'], + filterableSubFields: [ + 'primaryLinkUrl', + 'primaryLinkLabel', + 'secondaryLinks', + ], labelBySubField: { primaryLinkUrl: 'Link URL', primaryLinkLabel: 'Link Label', diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts b/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts index efb088b7e..6138fe031 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts @@ -3,7 +3,7 @@ import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetViewGroupsFilters } from '@/views/hooks/useGetViewGroupsFilters'; diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts index 3d4e12f23..240bd3a59 100644 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts +++ b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts @@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; -import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; import { View } from '@/views/types/View'; import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';