From f0f0d85380c7b48f0e3ffee26ef05302cafa1f44 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 10 Jun 2025 14:00:12 +0200 Subject: [PATCH] Fixed sub-field filter dropdown content and icons (#12516) This PR improves the sub-field selection UX in advanced filters. - Now using the icon of the field instead of the field type - Now using the label of the field instead of the field type - Removed now useless constant ICON_NAME_BY_ANY_SUB_FIELD - Now selects a default value (any or default sub-field for type) when clicking on the field, instead of waiting for the user to select the sub-field Fixes https://github.com/twentyhq/core-team-issues/issues/1005 --- .../AdvancedFilterFieldSelectMenu.tsx | 10 +++--- .../AdvancedFilterSubFieldSelectMenu.tsx | 21 +++-------- ...SelectFieldUsedInAdvancedFilterDropdown.ts | 36 +++++++++++++++++-- .../constants/IconNameByAnySubField.ts | 13 ------- ...ompositeTypeNonFilterableByAnySubField.ts} | 4 +-- 5 files changed, 44 insertions(+), 40 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameByAnySubField.ts rename packages/twenty-front/src/modules/object-record/record-filter/utils/{isCompositeTypeFilterableByAnySubField.ts => isCompositeTypeNonFilterableByAnySubField.ts} (72%) diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx index f0eff3bab..603f44d1b 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx @@ -106,17 +106,17 @@ export const AdvancedFilterFieldSelectMenu = ({ selectedFieldMetadataItem.type, ); + selectFieldUsedInAdvancedFilterDropdown({ + fieldMetadataItemId: selectedFieldMetadataItem.id, + recordFilterId, + }); + if (isCompositeFieldType(filterType)) { setObjectFilterDropdownSubMenuFieldType(filterType); setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); setObjectFilterDropdownIsSelectingCompositeField(true); } else { - selectFieldUsedInAdvancedFilterDropdown({ - fieldMetadataItemId: selectedFieldMetadataItem.id, - recordFilterId, - }); - closeAdvancedFilterFieldSelectDropdown(); } }; 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 36cbe7edd..e693d1606 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 @@ -10,12 +10,9 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; -import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; -import { DEFAULT_ANY_SUB_FIELD_ICON_NAME } from '@/object-record/record-filter/constants/DefaultAnySubFieldIconName'; -import { ICON_NAME_BY_ANY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameByAnySubField'; import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField'; import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable'; -import { isCompositeTypeFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField'; +import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -103,7 +100,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({ const compositeFieldTypeIsFilterableByAnySubField = isDefined(fieldMetadataItemUsedInDropdown) && - isCompositeTypeFilterableByAnySubField( + !isCompositeTypeNonFilterableByAnySubField( fieldMetadataItemUsedInDropdown.type, ); @@ -112,16 +109,6 @@ export const AdvancedFilterSubFieldSelectMenu = ({ ...subFieldNames.map((subFieldName) => subFieldName), ]; - const iconNameForAnySubField = isDefined(fieldMetadataItemUsedInDropdown) - ? ICON_NAME_BY_ANY_SUB_FIELD[fieldMetadataItemUsedInDropdown?.type] - : (null ?? null); - - const anySubFieldIcon = getIcon( - iconNameForAnySubField ?? - fieldMetadataItemUsedInDropdown?.icon ?? - DEFAULT_ANY_SUB_FIELD_ICON_NAME, - ); - return ( } > - {getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} + {fieldMetadataItemUsedInDropdown?.label} { handleSelectFilter(fieldMetadataItemUsedInDropdown); }} - LeftIcon={anySubFieldIcon} + LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)} text={`Any ${fieldMetadataItemUsedInDropdown.label} field`} /> diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts index bc8126457..1ed0f6ea9 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts @@ -6,10 +6,13 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField'; import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; @@ -81,7 +84,13 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { const firstOperand = getRecordFilterOperands({ filterType, subFieldName, - })[0]; + })?.[0]; + + if (!isDefined(firstOperand)) { + throw new Error( + `No valid operand found for filter type: ${filterType} and subFieldName: ${subFieldName}`, + ); + } setSelectedOperandInDropdown(firstOperand); @@ -94,6 +103,27 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { (recordFilter) => recordFilter.id === recordFilterId, ); + const isCompositeFilterOnAnySubField = + isCompositeFieldType(filterType) && !isDefined(subFieldName); + const compositeFilterNonFilterableByAnySubField = + isCompositeTypeNonFilterableByAnySubField(filterType); + + let subFieldNameForNonFilterableWithAny: + | CompositeFieldSubFieldName + | undefined + | null = subFieldName; + + if ( + isCompositeFilterOnAnySubField && + compositeFilterNonFilterableByAnySubField + ) { + subFieldNameForNonFilterableWithAny = + getDefaultSubFieldNameForCompositeFilterableFieldType(filterType); + } + + const subFieldNameToUse = + subFieldName ?? subFieldNameForNonFilterableWithAny; + const newAdvancedFilter = { id: recordFilterId, fieldMetadataId: fieldMetadataItem.id, @@ -105,10 +135,10 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { existingRecordFilter?.positionInRecordFilterGroup, type: filterType, label: fieldMetadataItem.label, - subFieldName, + subFieldName: subFieldNameToUse, } satisfies RecordFilter; - setSubFieldNameUsedInDropdown(subFieldName); + setSubFieldNameUsedInDropdown(subFieldNameToUse); setObjectFilterDropdownSearchInput(''); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameByAnySubField.ts b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameByAnySubField.ts deleted file mode 100644 index e31dbdb40..000000000 --- a/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameByAnySubField.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -export const ICON_NAME_BY_ANY_SUB_FIELD: Partial< - Record -> = { - [FieldMetadataType.LINKS]: 'IconLink', - [FieldMetadataType.EMAILS]: 'IconMail', - [FieldMetadataType.PHONES]: 'IconPhone', - [FieldMetadataType.ADDRESS]: 'IconMap', - [FieldMetadataType.ACTOR]: 'IconCreativeCommonsSa', - [FieldMetadataType.FULL_NAME]: 'IconUser', - [FieldMetadataType.POSITION]: 'IconBriefcase', -}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField.ts similarity index 72% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField.ts index deb63ac3b..4d3fb2203 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField.ts @@ -8,8 +8,8 @@ const COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY = [ type CompositeTypeNonFilterableWithAny = (typeof COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY)[number]; -export const isCompositeTypeFilterableByAnySubField = ( +export const isCompositeTypeNonFilterableByAnySubField = ( fieldType: FieldType, ): fieldType is CompositeTypeNonFilterableWithAny => { - return !COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY.includes(fieldType as any); + return COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY.includes(fieldType as any); };