From 3308ba56b25e708a5598aee46c7ee2aaa32ebe9a Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 9 May 2025 17:49:04 +0200 Subject: [PATCH] Implemented ACTOR sub-field filtering (#11957) This PR implements what's missing for ACTOR sub-field filtering, filtering on the source sub-field was already working. We can now filter on name sub-field. Since the sub-fields are different types and cannot be filtered both by text, we consider that a simple filter on ACTOR is filtering on the source, we have to go to advanced filter to have the name filter sub-field. --- ...dvancedFilterRecordFilterOperandSelect.tsx | 3 +- .../components/AdvancedFilterValueInput.tsx | 38 ++-- .../graphql/types/RecordGqlOperationFilter.ts | 2 +- .../ObjectFilterDropdownFilterInput.tsx | 22 +- .../constants/IconNameBySubField.ts | 2 + .../computeViewRecordGqlOperationFilter.ts | 188 ++++++++++++------ .../SettingsCompositeFieldTypeConfigs.ts | 4 +- .../EditableFilterDropdownButton.tsx | 1 + 8 files changed, 167 insertions(+), 93 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx index fccd79aac..164f88aa7 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx @@ -94,7 +94,7 @@ export const AdvancedFilterRecordFilterOperandSelect = ({ /> } dropdownComponents={ - + ); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx index 3df320b64..7f5a52cc4 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx @@ -83,17 +83,31 @@ export const AdvancedFilterValueInput = ({ ? ({ y: -33, x: 0 } satisfies DropdownOffset) : DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET; - const showFilterTextInput = - (isDefined(filterType) && - (TEXT_FILTER_TYPES.includes(filterType) || - NUMBER_FILTER_TYPES.includes(filterType))) || - isExpectedSubFieldName( - FieldMetadataType.CURRENCY, - 'amountMicros', - recordFilter.subFieldName, - ) || - (filterType === 'ADDRESS' && - subFieldNameUsedInDropdown !== 'addressCountry'); + const isFilterableByTextValue = + isDefined(filterType) && + (TEXT_FILTER_TYPES.includes(filterType) || + NUMBER_FILTER_TYPES.includes(filterType)); + + const isCurrencyAmountMicrosFilter = isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'amountMicros', + recordFilter.subFieldName, + ); + + const isAddressFilterOnSubFieldOtherThanCountry = + filterType === 'ADDRESS' && subFieldNameUsedInDropdown !== 'addressCountry'; + + const isActorNameFilter = isExpectedSubFieldName( + FieldMetadataType.ACTOR, + 'name', + recordFilter.subFieldName, + ); + + const showFilterTextInputInsteadOfDropdown = + isFilterableByTextValue || + isCurrencyAmountMicrosFilter || + isAddressFilterOnSubFieldOtherThanCountry || + isActorNameFilter; return ( @@ -103,7 +117,7 @@ export const AdvancedFilterValueInput = ({ - ) : showFilterTextInput ? ( + ) : showFilterTextInputInsteadOfDropdown ? ( ) : ( )} - {filterType === 'ACTOR' && - (isActorSourceCompositeFilter || isNotASubFieldFilter ? ( - <> - - - ) : ( - <> - - - ))} + {filterType === 'ACTOR' && ( + <> + + + )} {filterType === 'ADDRESS' && (isNotASubFieldFilter ? ( <> 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 dca90d8c4..1fdebd24e 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 @@ -5,4 +5,6 @@ export const ICON_NAME_BY_SUB_FIELD: Partial< > = { currencyCode: 'IconCurrencyDollar', amountMicros: 'IconNumber95Small', + name: 'IconAlignJustified', + source: 'IconFileArrowLeft', }; 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/computeViewRecordGqlOperationFilter.ts index 9d1e9758f..a529780c3 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/computeViewRecordGqlOperationFilter.ts @@ -927,73 +927,139 @@ export const computeFilterRecordGqlOperationFilter = ({ ); } } - // TODO: fix this with a new composite field in ViewFilter entity case 'ACTOR': { - switch (filter.operand) { - case RecordFilterOperand.Is: { - if (filter.value === '[]') { - return; - } + if (isSubFieldFilter) { + switch (subFieldName) { + case 'source': { + switch (filter.operand) { + case RecordFilterOperand.Is: { + if (filter.value === '[]') { + return; + } - const parsedRecordIds = JSON.parse(filter.value) as string[]; + const parsedSources = JSON.parse(filter.value) as string[]; - return { - [correspondingField.name]: { - source: { - in: parsedRecordIds, - } as RelationFilter, - }, - }; - } - case RecordFilterOperand.IsNot: { - if (filter.value === '[]') { - return; - } - - const parsedRecordIds = JSON.parse(filter.value) as string[]; - - if (parsedRecordIds.length === 0) return; - - return { - not: { - [correspondingField.name]: { - source: { - in: parsedRecordIds, - } as RelationFilter, - }, - }, - }; - } - case RecordFilterOperand.Contains: - return { - or: [ - { - [correspondingField.name]: { - name: { - ilike: `%${filter.value}%`, - }, - } as ActorFilter, - }, - ], - }; - case RecordFilterOperand.DoesNotContain: - return { - and: [ - { - not: { + return { [correspondingField.name]: { - name: { - ilike: `%${filter.value}%`, + source: { + in: parsedSources, + } satisfies RelationFilter, + }, + }; + } + case RecordFilterOperand.IsNot: { + if (filter.value === '[]') { + return; + } + + const parsedSources = JSON.parse(filter.value) as string[]; + + if (parsedSources.length === 0) return; + + return { + not: { + [correspondingField.name]: { + source: { + in: parsedSources, + } satisfies RelationFilter, }, - } as ActorFilter, + }, + }; + } + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.label} filter`, + ); + } + } + case 'name': { + switch (filter.operand) { + case RecordFilterOperand.Contains: + return { + or: [ + { + [correspondingField.name]: { + name: { + ilike: `%${filter.value}%`, + }, + } satisfies ActorFilter, + }, + ], + }; + case RecordFilterOperand.DoesNotContain: + return { + and: [ + { + not: { + [correspondingField.name]: { + name: { + ilike: `%${filter.value}%`, + }, + } satisfies ActorFilter, + }, + }, + ], + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.label} filter`, + ); + } + } + } + break; + } else { + if (filter.value === '[]') { + return; + } + + const parsedSources = JSON.parse(filter.value) as string[]; + + if (parsedSources.length === 0) return; + + switch (filter.operand) { + case RecordFilterOperand.Contains: + return { + or: [ + { + [correspondingField.name]: { + source: { + in: parsedSources, + }, + } satisfies ActorFilter, }, - }, - ], - }; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.label} filter`, - ); + ], + }; + case RecordFilterOperand.DoesNotContain: + return { + and: [ + { + or: [ + { + not: { + [correspondingField.name]: { + source: { + in: parsedSources, + }, + } satisfies ActorFilter, + }, + }, + { + [correspondingField.name]: { + source: { + is: 'NULL', + }, + } satisfies ActorFilter, + }, + ], + }, + ], + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.label} filter`, + ); + } } } case 'EMAILS': 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 0f700d998..ef8bd887d 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 @@ -177,8 +177,8 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { label: 'Actor', Icon: IllustrationIconSetting, category: 'Basic', - subFields: ['source'], - filterableSubFields: ['source'], + subFields: ['source', 'name'], + filterableSubFields: ['source', 'name'], labelBySubField: { source: 'Source', name: 'Name', diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index b7225195e..89d4e0f6d 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -65,6 +65,7 @@ export const EditableFilterDropdownButton = ({ dropdownOffset={{ y: 8, x: 0 }} dropdownPlacement="bottom-start" onClickOutside={handleDropdownClickOutside} + dropdownWidth={200} /> );