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';