From 50cb32d1227364ba108e5b05eccc2bf26421998f Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 25 Apr 2025 19:33:00 +0200 Subject: [PATCH] Implement sub-field filtering on CURRENCY field type (#11726) This PR implements sub-field filtering on CURRENCY field type and improves many related zones. - Created a ObjectFilterDropdownCurrencySelect dropdown component for filtering on multiple currencies - Added currencyCode sub-field to CurrencyFilter type - Created getDefaultSubFieldNameForCompositeFilterableFieldType to avoid situation where we don't have any sub field name in sub field filtering situations. - Implemented filtering for currencyCode in computeFilterRecordGqlOperationFilter - Implemented CURRENCY type in getRecordFilterOperands - Implemented isMatchingCurrencyFilter for using in isRecordMatchingFilter for proper optimistic rendering - Created turnCurrencyIntoSelectableItem to help ObjectFilterDropdownCurrencySelect Testing : - Added test for currency sub fields in getOperandsForFilterType - Completely reworked isMatchingCurrencyFilter test Improvements : - Created a unique CURRENCIES constant to avoid re-creating it at various places - Derive the type FilterableFieldType from a constant array FILTERABLE_FIELD_TYPES, so it's easier to work with - Added areCompositeTypeSubFieldsFilterable - Fixed a bug with empty value '[]' that was preventing the auto-removal of a filter chip Miscellaneous : - Created isExpectedSubFieldName util to do a type-safe check of a subFieldName - Better naming : renamed isCompositeField to isCompositeFieldType - Created isCompositeTypeFilterableWithAny to specify which field types are filterable by any sub field - Better naming : renamed ObjectFilterDropdownFilterSelectCompositeFieldSubMenu to ObjectFilterDropdownSubFieldSelect - Better naming : renamed ObjectFilterDropdownFilterSelect to ObjectFilterDropdownFieldSelect - Created isEmptinessOperand util instead of duplicating the same hard-coded check in multiple places - Better naming : used subFieldName instead of compositeFieldName for consistency - UseEffect removal : removed unnecessary useEffect in MultipleSelectDropdown Fixes a bug where Empty and Not weren't appearing in filter chip in particular cases Fixes https://github.com/twentyhq/core-team-issues/issues/498 Fixes https://github.com/twentyhq/twenty/issues/7558 --- .../AdvancedFilterDropdownFilterInput.tsx | 15 + ...eldSelectDropdownButtonClickableSelect.tsx | 4 +- .../AdvancedFilterFieldSelectMenu.tsx | 4 +- .../AdvancedFilterSubFieldSelectMenu.tsx | 63 ++- .../components/AdvancedFilterValueInput.tsx | 16 +- .../graphql/types/RecordGqlOperationFilter.ts | 1 + .../MultipleFiltersDropdownContent.tsx | 8 +- .../ObjectFilterDropdownCurrencySelect.tsx | 220 +++++++++ ...sx => ObjectFilterDropdownFieldSelect.tsx} | 6 +- .../ObjectFilterDropdownFilterInput.tsx | 23 + ...jectFilterDropdownFilterSelectMenuItem.tsx | 8 +- ...ctFilterDropdownFilterSelectMenuItemV2.tsx | 6 +- .../ObjectFilterDropdownNumberInput.tsx | 7 +- ...=> ObjectFilterDropdownSubFieldSelect.tsx} | 60 ++- .../constants/NumberFilterTypes.ts | 2 +- .../hooks/useSelectFilterUsedInDropdown.ts | 8 +- .../getOperandsForFilterType.test.ts | 37 +- ...positeField.ts => isCompositeFieldType.ts} | 5 +- .../utils/isCompositeFilterableFieldType.ts | 10 + .../utils/isExpectedSubFieldName.ts | 20 + .../utils/turnCurrencyIntoSelectableItem.ts | 12 + ...ptionsDropdownRecordGroupFieldsContent.tsx | 2 +- .../components/FormCurrencyFieldInput.tsx | 12 +- .../constants/IconNameBySubField.ts | 8 + .../types/FilterableFieldType.ts | 42 +- ...omputeViewRecordGqlOperationFilter.test.ts | 174 +++++++ .../isMatchingCurrencyFilter.test.ts | 458 ++++++++++++------ .../areCompositeTypeSubFieldsFilterable.ts | 15 + .../computeViewRecordGqlOperationFilter.ts | 133 +++-- ...ieldNameForCompositeFilterableFieldType.ts | 34 ++ .../utils/getRecordFilterOperands.ts | 53 +- .../utils/isCompositeFieldTypeFilterable.ts | 11 - .../isCompositeTypeFilterableByAnySubField.ts | 15 + .../record-filter/utils/isEmptinessOperand.ts | 7 + .../utils/isMatchingCurrencyFilter.ts | 124 +++-- .../utils/isRecordFilterConsideredEmpty.ts | 2 +- .../utils/isRecordMatchingFilter.ts | 2 +- .../hooks/useHandleToggleColumnFilter.ts | 8 + .../utils/buildRecordInputFromFilter.ts | 4 +- .../components/MultipleSelectDropdown.tsx | 14 +- .../data-model/constants/Currencies.ts | 10 + .../SettingsCompositeFieldTypeConfigs.ts | 4 +- .../SettingsDataModelFieldCurrencyForm.tsx | 13 +- ...ColumnSelectFieldSelectDropdownContent.tsx | 4 +- ...umnSelectSubFieldSelectDropdownContent.tsx | 4 +- .../components/MatchColumnToFieldSelect.tsx | 12 +- .../field/input/components/CurrencyInput.tsx | 28 +- .../CurrencyPickerDropdownButton.tsx | 21 +- .../CurrencyPickerDropdownSelect.tsx | 12 +- .../components/internal/types/Currency.ts | 5 + .../views/components/EditableFilterChip.tsx | 14 +- 51 files changed, 1358 insertions(+), 422 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/{ObjectFilterDropdownFilterSelect.tsx => ObjectFilterDropdownFieldSelect.tsx} (97%) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/{ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx => ObjectFilterDropdownSubFieldSelect.tsx} (85%) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/{isCompositeField.ts => isCompositeFieldType.ts} (57%) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCurrencyIntoSelectableItem.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isEmptinessOperand.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/constants/Currencies.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/types/Currency.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx index b428c3b90..9c2de5a91 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx @@ -7,12 +7,15 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { AdvancedFilterDropdownDateInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput'; import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect'; +import { ObjectFilterDropdownCurrencySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect'; import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput'; import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; +import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { FieldMetadataType } from 'twenty-shared/types'; type AdvancedFilterDropdownFilterInputProps = { filterDropdownId?: string; @@ -65,6 +68,18 @@ export const AdvancedFilterDropdownFilterInput = ({ )} {filterType === 'BOOLEAN' && } + {filterType === 'CURRENCY' && + (isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'currencyCode', + recordFilter.subFieldName, + ) ? ( + <> + + + ) : ( + <> + ))} ); }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect.tsx index 00c9f3544..4b3270489 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect.tsx @@ -1,6 +1,6 @@ import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName'; import { SelectControl } from '@/ui/input/components/SelectControl'; @@ -38,7 +38,7 @@ export const AdvancedFilterFieldSelectDropdownButtonClickableSelect = ({ const subFieldLabel = isDefined(fieldMetadataItem) && - isCompositeField(fieldMetadataItem.type) && + isCompositeFieldType(fieldMetadataItem.type) && isNonEmptyString(recordFilter?.subFieldName) && isValidSubFieldName(recordFilter.subFieldName) ? getCompositeSubFieldLabel( 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 e194f5b29..d949beb88 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 @@ -19,7 +19,7 @@ import { ObjectFilterDropdownFilterSelectMenuItemV2 } from '@/object-record/obje import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -103,7 +103,7 @@ export const AdvancedFilterFieldSelectMenu = ({ selectedFieldMetadataItem.type, ); - if (isCompositeField(filterType)) { + if (isCompositeFieldType(filterType)) { setObjectFilterDropdownSubMenuFieldType(filterType); setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); 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 13cb22dfd..f57b3e92c 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 @@ -11,7 +11,9 @@ import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/o 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 { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/isCompositeFieldTypeFilterable'; +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 { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -87,19 +89,23 @@ export const AdvancedFilterSubFieldSelectMenu = ({ return null; } - const options = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ + const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ objectFilterDropdownSubMenuFieldType ].filterableSubFields.sort((a, b) => a.localeCompare(b)); const subFieldsAreFilterable = isDefined(fieldMetadataItemUsedInDropdown) && - isCompositeFieldTypeSubFieldsFilterable( + areCompositeTypeSubFieldsFilterable(fieldMetadataItemUsedInDropdown.type); + + const compositeFieldTypeIsFilterableByAnySubField = + isDefined(fieldMetadataItemUsedInDropdown) && + isCompositeTypeFilterableByAnySubField( fieldMetadataItemUsedInDropdown.type, ); const selectableItemIdArray = [ '-1', - ...options.map((subFieldName) => subFieldName), + ...subFieldNames.map((subFieldName) => subFieldName), ]; return ( @@ -120,24 +126,28 @@ export const AdvancedFilterSubFieldSelectMenu = ({ selectableItemIdArray={selectableItemIdArray} selectableListInstanceId={advancedFilterFieldSelectDropdownId} > - { - handleSelectFilter(fieldMetadataItemUsedInDropdown); - }} - > - { + {compositeFieldTypeIsFilterableByAnySubField && ( + { handleSelectFilter(fieldMetadataItemUsedInDropdown); }} - LeftIcon={IconApps} - text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`} - /> - + > + { + handleSelectFilter(fieldMetadataItemUsedInDropdown); + }} + LeftIcon={IconApps} + text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`} + /> + + )} {subFieldsAreFilterable && - options.map((subFieldName, index) => ( + subFieldNames.map((subFieldName, index) => ( { - handleSelectFilter( - fieldMetadataItemUsedInDropdown, - subFieldName, - ); + if (isDefined(fieldMetadataItemUsedInDropdown)) { + handleSelectFilter( + fieldMetadataItemUsedInDropdown, + subFieldName, + ); + } }} text={getCompositeSubFieldLabel( objectFilterDropdownSubMenuFieldType, subFieldName, )} - LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)} + LeftIcon={getIcon( + ICON_NAME_BY_SUB_FIELD[subFieldName] ?? + fieldMetadataItemUsedInDropdown?.icon, + )} /> ))} 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 fce8ea9c2..bd482bfab 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 @@ -6,6 +6,7 @@ import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/cons import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes'; import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands'; +import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset'; @@ -13,6 +14,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; const StyledValueDropdownContainer = styled.div` @@ -60,6 +62,16 @@ 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, + ); + return ( {operandHasNoInput ? ( @@ -68,9 +80,7 @@ export const AdvancedFilterValueInput = ({ - ) : isDefined(filterType) && - (TEXT_FILTER_TYPES.includes(filterType) || - NUMBER_FILTER_TYPES.includes(filterType)) ? ( + ) : showFilterTextInput ? ( ) : ( ) : shouldShowCompositeSelectionSubMenu ? ( - + ) : ( - + )} ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx new file mode 100644 index 000000000..0893eead3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx @@ -0,0 +1,220 @@ +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { turnCurrencyIntoSelectableItem } from '@/object-record/object-filter-dropdown/utils/turnCurrencyIntoSelectableItem'; +import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; +import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { useLingui } from '@lingui/react/macro'; +import { ChangeEvent, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation'; +import { v4 } from 'uuid'; + +export const EMPTY_FILTER_VALUE = '[]'; +export const MAX_ITEMS_TO_DISPLAY = 3; + +type ObjectFilterDropdownCurrencySelectProps = { + viewComponentId?: string; + dropdownWidth?: number; +}; + +export const ObjectFilterDropdownCurrencySelect = ({ + viewComponentId, + dropdownWidth, +}: ObjectFilterDropdownCurrencySelectProps) => { + const [searchText, setSearchText] = useState(''); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const setObjectFilterDropdownSelectedRecordIds = useSetRecoilComponentStateV2( + objectFilterDropdownSelectedRecordIdsComponentState, + selectedFilter?.id, + ); + + const objectFilterDropdownSelectedRecordIds = useRecoilComponentValueV2( + objectFilterDropdownSelectedRecordIdsComponentState, + selectedFilter?.id, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); + + const currenciesAsSelectableItems = CURRENCIES.map( + turnCurrencyIntoSelectableItem, + ); + + const filteredSelectableItems = currenciesAsSelectableItems.filter( + (selectableItem) => + selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) && + !objectFilterDropdownSelectedRecordIds.includes(selectableItem.id), + ); + + const filteredSelectedItems = currenciesAsSelectableItems.filter( + (selectableItem) => + selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) && + objectFilterDropdownSelectedRecordIds.includes(selectableItem.id), + ); + + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + const handleMultipleItemSelectChange = ( + itemToSelect: SelectableItem, + newSelectedValue: boolean, + ) => { + const newSelectedItemIds = newSelectedValue + ? [...objectFilterDropdownSelectedRecordIds, itemToSelect.id] + : objectFilterDropdownSelectedRecordIds.filter( + (id) => id !== itemToSelect.id, + ); + + if (!isDefined(fieldMetadataItemUsedInFilterDropdown)) { + throw new Error( + 'Field metadata item used in filter dropdown should be defined', + ); + } + + setObjectFilterDropdownSelectedRecordIds(newSelectedItemIds); + + const selectedItemNames = currenciesAsSelectableItems + .filter((option) => newSelectedItemIds.includes(option.id)) + .map((option) => option.name); + + const filterDisplayValue = + selectedItemNames.length > MAX_ITEMS_TO_DISPLAY + ? `${selectedItemNames.length} currencies` + : selectedItemNames.join(', '); + + if ( + isDefined(fieldMetadataItemUsedInFilterDropdown) && + isDefined(selectedOperandInDropdown) + ) { + const newFilterValue = + newSelectedItemIds.length > 0 + ? JSON.stringify(newSelectedItemIds) + : EMPTY_FILTER_VALUE; + + const duplicateFilterInCurrentRecordFilters = + findDuplicateRecordFilterInNonAdvancedRecordFilters({ + recordFilters: currentRecordFilters, + fieldMetadataItemId: fieldMetadataItemUsedInFilterDropdown.id, + subFieldName: 'currencyCode', + }); + + const filterIsAlreadyInCurrentRecordFilters = isDefined( + duplicateFilterInCurrentRecordFilters, + ); + + const filterId = filterIsAlreadyInCurrentRecordFilters + ? duplicateFilterInCurrentRecordFilters?.id + : v4(); + + applyRecordFilter({ + id: selectedFilter?.id ? selectedFilter.id : filterId, + type: getFilterTypeFromFieldType( + fieldMetadataItemUsedInFilterDropdown.type, + ), + label: fieldMetadataItemUsedInFilterDropdown.label, + operand: selectedOperandInDropdown || ViewFilterOperand.Is, + displayValue: filterDisplayValue, + fieldMetadataId: fieldMetadataItemUsedInFilterDropdown.id, + value: newFilterValue, + recordFilterGroupId: selectedFilter?.recordFilterGroupId, + subFieldName: 'currencyCode', + positionInRecordFilterGroup: + selectedFilter?.positionInRecordFilterGroup, + }); + } + }; + + const showNoResult = + filteredSelectableItems.length === 0 && + filteredSelectedItems.length === 0 && + searchText !== ''; + + const { t } = useLingui(); + + return ( + <> + ) => { + setSearchText(event.target.value); + }} + /> + + + {filteredSelectedItems?.map((item) => { + return ( + { + handleMultipleItemSelectChange(item, newCheckedValue); + }} + avatar={ + + } + /> + ); + })} + {filteredSelectableItems?.map((item) => { + return ( + { + handleMultipleItemSelectChange(item, newCheckedValue); + }} + avatar={ + + } + /> + ); + })} + {showNoResult && } + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx similarity index 97% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx index 0e3963eb2..25a302e21 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx @@ -47,13 +47,13 @@ export const StyledInput = styled.input` } `; -type ObjectFilterDropdownFilterSelectProps = { +type ObjectFilterDropdownFieldSelectProps = { isAdvancedFilterButtonVisible?: boolean; }; -export const ObjectFilterDropdownFilterSelect = ({ +export const ObjectFilterDropdownFieldSelect = ({ isAdvancedFilterButtonVisible, -}: ObjectFilterDropdownFilterSelectProps) => { +}: ObjectFilterDropdownFieldSelectProps) => { const { recordIndexId } = useRecordIndexContextOrThrow(); const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] = diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx index 3580d3f3d..356ec719a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx @@ -10,6 +10,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect'; +import { ObjectFilterDropdownCurrencySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect'; import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput'; import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes'; import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes'; @@ -17,8 +18,10 @@ import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/consta import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; +import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; type ObjectFilterDropdownFilterInputProps = { @@ -105,6 +108,26 @@ export const ObjectFilterDropdownFilterInput = ({ ))} + {filterType === 'CURRENCY' && + (isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'currencyCode', + subFieldNameUsedInDropdown, + ) ? ( + <> + + + ) : isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'amountMicros', + subFieldNameUsedInDropdown, + ) ? ( + <> + + + ) : ( + <> + ))} {['SELECT', 'MULTI_SELECT'].includes(filterType) && ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index be665e28f..f6eba8512 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -9,7 +9,7 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object- import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; @@ -113,7 +113,9 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ const Icon = getIcon(fieldMetadataItemToSelect.icon); - const shouldShowSubMenu = isCompositeField(fieldMetadataItemToSelect.type); + const shouldShowSubMenu = isCompositeFieldType( + fieldMetadataItemToSelect.type, + ); const handleClick = () => { resetSelectedItem(); @@ -122,7 +124,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ fieldMetadataItemToSelect.type, ); - if (isCompositeField(filterType)) { + if (isCompositeFieldType(filterType)) { setObjectFilterDropdownSubMenuFieldType(filterType); setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemToSelect.id); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItemV2.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItemV2.tsx index bd7b4e59e..0aa29bb70 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItemV2.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItemV2.tsx @@ -1,7 +1,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; @@ -28,7 +28,9 @@ export const ObjectFilterDropdownFilterSelectMenuItemV2 = ({ const Icon = getIcon(fieldMetadataItemToSelect.icon); - const shouldShowSubMenu = isCompositeField(fieldMetadataItemToSelect.type); + const shouldShowSubMenu = isCompositeFieldType( + fieldMetadataItemToSelect.type, + ); const handleClick = () => { resetSelectedItem(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx index 129e2c722..3f69de42e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx @@ -5,6 +5,7 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -23,6 +24,10 @@ export const ObjectFilterDropdownNumberInput = () => { selectedFilterComponentState, ); + const subFieldNameUsedInDropdown = useRecoilComponentValueV2( + subFieldNameUsedInDropdownComponentState, + ); + const { applyRecordFilter } = useApplyRecordFilter(); const [hasFocused, setHasFocused] = useState(false); @@ -70,7 +75,7 @@ export const ObjectFilterDropdownNumberInput = () => { recordFilterGroupId: selectedFilter?.recordFilterGroupId, positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup, - subFieldName: selectedFilter?.subFieldName, + subFieldName: subFieldNameUsedInDropdown, }); }} /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx similarity index 85% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx index 855586469..f2720a2b9 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx @@ -1,5 +1,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; @@ -13,10 +14,12 @@ import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; +import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable'; import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; -import { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/isCompositeFieldTypeFilterable'; +import { isCompositeTypeFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -32,8 +35,8 @@ import { isDefined } from 'twenty-shared/utils'; import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { - const [searchText] = useState(''); +export const ObjectFilterDropdownSubFieldSelect = () => { + const [searchText, setSearchText] = useState(''); const { getIcon } = useIcons(); @@ -154,7 +157,11 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { const subFieldsAreFilterable = isDefined(fieldMetadataItemUsedInDropdown) && - isCompositeFieldTypeSubFieldsFilterable( + areCompositeTypeSubFieldsFilterable(fieldMetadataItemUsedInDropdown.type); + + const compositeFieldTypeFilterableByAnySubField = + isDefined(fieldMetadataItemUsedInDropdown) && + isCompositeTypeFilterableByAnySubField( fieldMetadataItemUsedInDropdown.type, ); @@ -170,41 +177,41 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { > {getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} - {/* ) => setSearchText(event.target.value) } - /> */} + /> - { - handleSelectFilter(fieldMetadataItemUsedInDropdown); - }} - > - { + onEnter={() => { handleSelectFilter(fieldMetadataItemUsedInDropdown); }} - LeftIcon={IconApps} - text={`Any ${getFilterableFieldTypeLabel( - objectFilterDropdownSubMenuFieldType, - )} field`} - /> - - + > + { + handleSelectFilter(fieldMetadataItemUsedInDropdown); + }} + LeftIcon={IconApps} + text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`} + /> + + ) : ( + <> + )} {subFieldsAreFilterable && options.map((subFieldName, index) => ( { objectFilterDropdownSubMenuFieldType, subFieldName, )} - LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)} + LeftIcon={getIcon( + ICON_NAME_BY_SUB_FIELD[subFieldName] ?? + fieldMetadataItemUsedInDropdown?.icon, + )} /> ))} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/NumberFilterTypes.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/NumberFilterTypes.ts index c17732c2d..24da6abba 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/NumberFilterTypes.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/NumberFilterTypes.ts @@ -1 +1 @@ -export const NUMBER_FILTER_TYPES = ['NUMBER', 'CURRENCY', 'PHONES']; +export const NUMBER_FILTER_TYPES = ['NUMBER', 'PHONES']; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts index 68ae3a723..a7c8ed06f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts @@ -8,10 +8,11 @@ import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApp import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; type SelectFilterParams = { fieldMetadataItemId: string; @@ -59,8 +60,12 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => { const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + const defaultSubFieldName = + getDefaultSubFieldNameForCompositeFilterableFieldType(filterType); + const firstOperand = getRecordFilterOperands({ filterType, + subFieldName: defaultSubFieldName, })[0]; setSelectedOperandInDropdown(firstOperand); @@ -79,6 +84,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => { value, type: filterType, label: fieldMetadataItem.label, + subFieldName: defaultSubFieldName, }); } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts index dbe5c921f..fc7c63e35 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts @@ -1,6 +1,8 @@ import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { FieldType } from '@/settings/data-model/types/FieldType'; describe('getOperandsForFilterType', () => { const emptyOperands = [ @@ -18,6 +20,18 @@ describe('getOperandsForFilterType', () => { RecordFilterOperand.LessThan, ]; + const currencyAmountMicrosOperands = [ + RecordFilterOperand.GreaterThan, + RecordFilterOperand.LessThan, + RecordFilterOperand.Is, + RecordFilterOperand.IsNot, + ]; + + const currencyCurrencyCodeOperands = [ + RecordFilterOperand.Is, + RecordFilterOperand.IsNot, + ]; + const dateOperands = [ RecordFilterOperand.Is, RecordFilterOperand.IsRelative, @@ -36,7 +50,16 @@ describe('getOperandsForFilterType', () => { ['ADDRESS', [...containsOperands, ...emptyOperands]], ['LINKS', [...containsOperands, ...emptyOperands]], ['ACTOR', [...containsOperands, ...emptyOperands]], - ['CURRENCY', [...numberOperands, ...emptyOperands]], + [ + 'CURRENCY', + [...currencyCurrencyCodeOperands, ...emptyOperands], + 'currencyCode', + ], + [ + 'CURRENCY', + [...currencyAmountMicrosOperands, ...emptyOperands], + 'amountMicros', + ], ['NUMBER', [...numberOperands, ...emptyOperands]], ['DATE', [...dateOperands, ...emptyOperands]], ['DATE_TIME', [...dateOperands, ...emptyOperands]], @@ -44,12 +67,20 @@ describe('getOperandsForFilterType', () => { [undefined, []], [null, []], ['UNKNOWN_TYPE', []], - ]; + ] satisfies ( + | [ + FieldType | null | undefined | 'UNKNOWN_TYPE', + RecordFilterOperand[], + CompositeFieldSubFieldName, + ] + | [FieldType | null | undefined | 'UNKNOWN_TYPE', RecordFilterOperand[]] + )[]; - testCases.forEach(([filterType, expectedOperands]) => { + testCases.forEach(([filterType, expectedOperands, subFieldName]) => { it(`should return correct operands for FilterType.${filterType}`, () => { const result = getRecordFilterOperands({ filterType: filterType as FilterableFieldType, + subFieldName, }); expect(result).toEqual(expectedOperands); }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFieldType.ts similarity index 57% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFieldType.ts index 6de44cd44..69d5659f9 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFieldType.ts @@ -4,5 +4,6 @@ import { } from '@/settings/data-model/types/CompositeFieldType'; import { FieldType } from '@/settings/data-model/types/FieldType'; -export const isCompositeField = (type: FieldType): type is CompositeFieldType => - COMPOSITE_FIELD_TYPES.includes(type as any); +export const isCompositeFieldType = ( + type: FieldType, +): type is CompositeFieldType => COMPOSITE_FIELD_TYPES.includes(type as any); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType.ts new file mode 100644 index 000000000..dedb038cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType.ts @@ -0,0 +1,10 @@ +import { CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType'; +import { FILTERABLE_FIELD_TYPES } from '@/object-record/record-filter/types/FilterableFieldType'; +import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFieldType'; +import { FieldType } from '@/settings/data-model/types/FieldType'; + +export const isCompositeFilterableFieldType = ( + type: FieldType, +): type is CompositeFilterableFieldType => + FILTERABLE_FIELD_TYPES.includes(type as any) && + COMPOSITE_FIELD_TYPES.includes(type as any); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts new file mode 100644 index 000000000..65cf31e04 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts @@ -0,0 +1,20 @@ +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; + +export const isExpectedSubFieldName = < + GivenFieldType extends keyof typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS, + CompositeFieldTypeSettings extends + typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS, + PossibleSubFieldsForGivenFieldType extends + CompositeFieldTypeSettings[GivenFieldType]['subFields'][number], +>( + fieldMetadataType: GivenFieldType, + subFieldName: PossibleSubFieldsForGivenFieldType, + subFieldNameToCheck: string | null | undefined, +): subFieldNameToCheck is PossibleSubFieldsForGivenFieldType => { + return ( + ( + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType] + .subFields as string[] + ).includes(subFieldName) && subFieldName === subFieldNameToCheck + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCurrencyIntoSelectableItem.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCurrencyIntoSelectableItem.ts new file mode 100644 index 000000000..f0d324b69 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCurrencyIntoSelectableItem.ts @@ -0,0 +1,12 @@ +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { Currency } from '@/ui/input/components/internal/types/Currency'; + +export const turnCurrencyIntoSelectableItem = ( + currency: Currency, +): SelectableItem => ({ + id: currency.value, + AvatarIcon: currency.Icon, + avatarType: 'icon', + name: `${currency.label}`, + isSelected: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index d34e08258..63c77a3a7 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; +import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCurrencyFieldInput.tsx index ae56f1caf..b0f810df2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCurrencyFieldInput.tsx @@ -5,7 +5,7 @@ import { FormSelectFieldInput } from '@/object-record/record-field/form-types/co import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { FormFieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; -import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { useMemo } from 'react'; import { IconCircleOff } from 'twenty-ui/display'; @@ -26,21 +26,13 @@ export const FormCurrencyFieldInput = ({ readonly, }: FormCurrencyFieldInputProps) => { const currencies = useMemo(() => { - const currencies = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( - ([key, { Icon, label }]) => ({ - value: key, - Icon, - label: `${label} (${key})`, - }), - ); - return [ { label: 'No currency', value: '', Icon: IconCircleOff, }, - ...currencies, + ...CURRENCIES, ]; }, []); 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 new file mode 100644 index 000000000..dca90d8c4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/constants/IconNameBySubField.ts @@ -0,0 +1,8 @@ +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; + +export const ICON_NAME_BY_SUB_FIELD: Partial< + Record +> = { + currencyCode: 'IconCurrencyDollar', + amountMicros: 'IconNumber95Small', +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/FilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/FilterableFieldType.ts index 9fc65b90e..b54b72b80 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/FilterableFieldType.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/FilterableFieldType.ts @@ -1,24 +1,30 @@ import { FieldType } from '@/settings/data-model/types/FieldType'; import { PickLiteral } from '~/types/PickLiteral'; +export const FILTERABLE_FIELD_TYPES = [ + 'TEXT', + 'PHONES', + 'EMAILS', + 'DATE_TIME', + 'DATE', + 'NUMBER', + 'CURRENCY', + 'FULL_NAME', + 'LINKS', + 'RELATION', + 'ADDRESS', + 'SELECT', + 'RATING', + 'MULTI_SELECT', + 'ACTOR', + 'ARRAY', + 'RAW_JSON', + 'BOOLEAN', +] as const; + +type FilterableFieldTypeBaseLiteral = (typeof FILTERABLE_FIELD_TYPES)[number]; + export type FilterableFieldType = PickLiteral< FieldType, - | 'TEXT' - | 'PHONES' - | 'EMAILS' - | 'DATE_TIME' - | 'DATE' - | 'NUMBER' - | 'CURRENCY' - | 'FULL_NAME' - | 'LINKS' - | 'RELATION' - | 'ADDRESS' - | 'SELECT' - | 'RATING' - | 'MULTI_SELECT' - | 'ACTOR' - | 'ARRAY' - | 'RAW_JSON' - | 'BOOLEAN' + FilterableFieldTypeBaseLiteral >; 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 41ac1a722..b2ce256be 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 @@ -1,3 +1,4 @@ +import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; 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'; @@ -934,6 +935,179 @@ describe('should work as expected for the different field types', () => { }); }); + it('currency amount micros sub field type', () => { + const companyMockARRFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'annualRecurringRevenue', + ); + + const ARRFilterIsGreaterThan: RecordFilter = { + id: 'company-ARR-filter-is-greater-than', + value: '1000', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: '1000', + operand: RecordFilterOperand.GreaterThan, + subFieldName: 'amountMicros' satisfies Extract< + keyof FieldCurrencyValue, + 'amountMicros' + >, + label: 'Amount', + type: FieldMetadataType.CURRENCY, + }; + + const ARRFilterIsLessThan: RecordFilter = { + id: 'company-ARR-filter-is-less-than', + value: '1000', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: '1000', + operand: RecordFilterOperand.LessThan, + subFieldName: 'amountMicros' satisfies Extract< + keyof FieldCurrencyValue, + 'amountMicros' + >, + label: 'Amount', + type: FieldMetadataType.CURRENCY, + }; + + const ARRFilterIs: RecordFilter = { + id: 'company-ARR-filter-is', + value: '1000', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: '1000', + operand: RecordFilterOperand.Is, + subFieldName: 'amountMicros' satisfies Extract< + keyof FieldCurrencyValue, + 'amountMicros' + >, + label: 'Amount', + type: FieldMetadataType.CURRENCY, + }; + + const ARRFilterIsNot: RecordFilter = { + id: 'company-ARR-filter-is-not', + value: '1000', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: '1000', + operand: RecordFilterOperand.IsNot, + subFieldName: 'amountMicros' satisfies Extract< + keyof FieldCurrencyValue, + 'amountMicros' + >, + label: 'Amount', + type: FieldMetadataType.CURRENCY, + }; + + const result = computeRecordGqlOperationFilter({ + filterValueDependencies: mockFilterValueDependencies, + recordFilters: [ + ARRFilterIsGreaterThan, + ARRFilterIsLessThan, + ARRFilterIs, + ARRFilterIsNot, + ], + recordFilterGroups: [], + fields: companyMockObjectMetadataItem.fields, + }); + + expect(result).toEqual({ + and: [ + { + annualRecurringRevenue: { + amountMicros: { + gte: 1000 * 1000000, + }, + }, + }, + { + annualRecurringRevenue: { + amountMicros: { + lte: 1000 * 1000000, + }, + }, + }, + { + annualRecurringRevenue: { + amountMicros: { + eq: 1000 * 1000000, + }, + }, + }, + { + not: { + annualRecurringRevenue: { + amountMicros: { + eq: 1000 * 1000000, + }, + }, + }, + }, + ], + }); + }); + + it('currency currency code sub field type', () => { + const companyMockARRFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'annualRecurringRevenue', + ); + + const ARRFilterIn: RecordFilter = { + id: 'company-ARR-filter-in', + value: '["USD"]', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: 'USD', + operand: RecordFilterOperand.Is, + subFieldName: 'currencyCode' satisfies Extract< + keyof FieldCurrencyValue, + 'currencyCode' + >, + label: 'Currency', + type: FieldMetadataType.CURRENCY, + }; + + const ARRFilterNotIn: RecordFilter = { + id: 'company-ARR-filter-not-in', + value: '["USD"]', + fieldMetadataId: companyMockARRFieldMetadataId?.id, + displayValue: 'Not USD', + operand: RecordFilterOperand.IsNot, + subFieldName: 'currencyCode' satisfies Extract< + keyof FieldCurrencyValue, + 'currencyCode' + >, + label: 'Currency', + type: FieldMetadataType.CURRENCY, + }; + + const result = computeRecordGqlOperationFilter({ + filterValueDependencies: mockFilterValueDependencies, + recordFilters: [ARRFilterIn, ARRFilterNotIn], + recordFilterGroups: [], + fields: companyMockObjectMetadataItem.fields, + }); + + expect(result).toEqual({ + and: [ + { + annualRecurringRevenue: { + currencyCode: { + in: ['USD'], + }, + }, + }, + { + not: { + annualRecurringRevenue: { + currencyCode: { + in: ['USD'], + }, + }, + }, + }, + ], + }); + }); + it('select field type with empty options', () => { const selectFieldMetadata = companyMockObjectMetadataItem.fields.find( (field) => field.type === FieldMetadataType.SELECT, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts index bee7fd8c0..73d8c4ad9 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts @@ -2,162 +2,326 @@ import { CurrencyFilter } from '@/object-record/graphql/types/RecordGqlOperation import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter'; describe('isMatchingCurrencyFilter', () => { - describe('eq', () => { - it('value equals eq filter', () => { + describe('amountMicros', () => { + describe('eq', () => { + it('value equals eq filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { eq: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { + amountMicros: 10, + }, + }), + ).toBe(true); + }); + + it('value does not equal eq filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { eq: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(false); + }); + }); + + describe('gt', () => { + it('value is greater than gt filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { gt: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(true); + }); + + it('value is not greater than gt filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { gt: 20 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(false); + }); + }); + + describe('gte', () => { + it('value is greater than or equal to gte filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { gte: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(true); + }); + + it('value is not greater than or equal to gte filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { gte: 20 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(false); + }); + }); + + describe('lt', () => { + it('value is less than lt filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { lt: 20 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(true); + }); + + it('value is not less than lt filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { lt: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(false); + }); + }); + + describe('lte', () => { + it('value is less than or equal to lte filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { lte: 20 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(true); + }); + + it('value is not less than or equal to lte filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { lte: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(false); + }); + }); + + describe('neq', () => { + it('value does not equal neq filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { neq: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 20 }, + }), + ).toBe(true); + }); + + it('value equals neq filter', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { neq: 10 }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(false); + }); + }); + + describe('is', () => { + it('value is NULL', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { is: 'NULL' }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: null as any }, + }), + ).toBe(true); + }); + + it('value is NOT_NULL', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { is: 'NOT_NULL' }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toBe(true); + }); + }); + }); + + describe('currencyCode', () => { + describe('in', () => { + it('value is in filter array', () => { + const currencyFilter: CurrencyFilter = { + currencyCode: { in: ['USD'] }, + }; + + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { currencyCode: 'USD' }, + }), + ).toBe(true); + }); + + it('value is not in filter array', () => { + const currencyFilter: CurrencyFilter = { + currencyCode: { in: ['USD'] }, + }; + + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { currencyCode: 'EUR' }, + }), + ).toBe(false); + }); + }); + + describe('is', () => { + it('value is NULL', () => { + const currencyFilter: CurrencyFilter = { + currencyCode: { is: 'NULL' }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { currencyCode: null as any }, + }), + ).toBe(true); + }); + + it('value is NOT_NULL', () => { + const currencyFilter: CurrencyFilter = { + currencyCode: { is: 'NOT_NULL' }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { currencyCode: 'USD' }, + }), + ).toBe(true); + }); + }); + }); + + describe('both filters', () => { + it('both filters match', () => { const currencyFilter: CurrencyFilter = { amountMicros: { eq: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - true, - ); - }); - - it('value does not equal eq filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { eq: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - false, - ); - }); - }); - - describe('gt', () => { - it('value is greater than gt filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { gt: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - true, - ); - }); - - it('value is not greater than gt filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { gt: 20 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - false, - ); - }); - }); - - describe('gte', () => { - it('value is greater than or equal to gte filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { gte: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - true, - ); - }); - - it('value is not greater than or equal to gte filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { gte: 20 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - false, - ); - }); - }); - - describe('in', () => { - it('value is in the array', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { in: [10, 20, 30] }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - true, - ); - }); - - it('value is not in the array', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { in: [10, 30, 40] }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - false, - ); - }); - }); - - describe('lt', () => { - it('value is less than lt filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { lt: 20 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - true, - ); - }); - - it('value is not less than lt filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { lt: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - false, - ); - }); - }); - - describe('lte', () => { - it('value is less than or equal to lte filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { lte: 20 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - true, - ); - }); - - it('value is not less than or equal to lte filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { lte: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - false, - ); - }); - }); - - describe('neq', () => { - it('value does not equal neq filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { neq: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 20 })).toBe( - true, - ); - }); - - it('value equals neq filter', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { neq: 10 }, - }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - false, - ); - }); - }); - - describe('is', () => { - it('value is NULL', () => { - const currencyFilter: CurrencyFilter = { - amountMicros: { is: 'NULL' }, + currencyCode: { in: ['USD'] }, }; expect( - isMatchingCurrencyFilter({ currencyFilter, value: null as any }), + isMatchingCurrencyFilter({ + currencyFilter, + value: { + amountMicros: 10, + currencyCode: 'USD', + }, + }), ).toBe(true); }); - it('value is NOT_NULL', () => { + it('amount micros filter does not match', () => { const currencyFilter: CurrencyFilter = { - amountMicros: { is: 'NOT_NULL' }, + amountMicros: { eq: 10 }, + currencyCode: { in: ['USD'] }, }; - expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe( - true, + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { + amountMicros: 20, + currencyCode: 'USD', + }, + }), + ).toBe(false); + }); + + it('currency code filter does not match', () => { + const currencyFilter: CurrencyFilter = { + amountMicros: { eq: 10 }, + currencyCode: { in: ['USD'] }, + }; + expect( + isMatchingCurrencyFilter({ + currencyFilter, + value: { + amountMicros: 10, + currencyCode: 'EUR', + }, + }), + ).toBe(false); + }); + }); + + describe('no filters', () => { + it('no filters match', () => { + const currencyFilter: CurrencyFilter = {}; + + expect(() => + isMatchingCurrencyFilter({ + currencyFilter, + value: { + amountMicros: 10, + currencyCode: 'USD', + }, + }), + ).toThrowError('Unexpected filter for currency : {}'); + }); + }); + + describe('unexpected operand', () => { + it('throws an error for unexpected operand', () => { + const currencyFilter: any = { + amountMicros: { unexpected: 10 }, + }; + expect(() => + isMatchingCurrencyFilter({ + currencyFilter, + value: { amountMicros: 10 }, + }), + ).toThrowError( + 'Unexpected operand for currency amount micros filter : {"unexpected":10}', ); }); }); 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 new file mode 100644 index 000000000..f46af31db --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts @@ -0,0 +1,15 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; + +const COMPOSITE_TYPES_FILTERABLE = [ + 'ACTOR', + 'FULL_NAME', + 'CURRENCY', +] satisfies FieldType[]; + +type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number]; + +export const areCompositeTypeSubFieldsFilterable = ( + fieldType: FieldType, +): fieldType is FilterableCompositeFieldType => { + return COMPOSITE_TYPES_FILTERABLE.includes(fieldType as any); +}; 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 af8a06b59..977984f34 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 @@ -39,9 +39,12 @@ import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/valid 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 { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; type ComputeFilterRecordGqlOperationFilterParams = { @@ -61,14 +64,11 @@ export const computeFilterRecordGqlOperationFilter = ({ (field) => field.id === filter.fieldMetadataId, ); - const compositeFieldName = filter.subFieldName; + const subFieldName = filter.subFieldName; - const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); + const isSubFieldFilter = isNonEmptyString(subFieldName); - const isEmptinessOperand = [ - RecordFilterOperand.IsEmpty, - RecordFilterOperand.IsNotEmpty, - ].includes(filter.operand); + const isAnEmptinessOperand = isEmptinessOperand(filter.operand); const isDateOperandWithoutValue = [ RecordFilterOperand.IsInPast, @@ -85,7 +85,7 @@ export const computeFilterRecordGqlOperationFilter = ({ const isFilterValueEmpty = !isDefined(filter.value) || filter.value === ''; const shouldSkipFiltering = - !isEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty; + !isAnEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty; if (shouldSkipFiltering) { return; @@ -98,7 +98,7 @@ export const computeFilterRecordGqlOperationFilter = ({ const filterHasEmptinessOperands = !filterTypesThatHaveNoEmptinessOperand.includes(filterType); - if (filterHasEmptinessOperands && isEmptinessOperand) { + if (filterHasEmptinessOperands && isAnEmptinessOperand) { const emptyOperationFilter = getEmptyRecordGqlOperationFilter({ operand: filter.operand, correspondingField, @@ -357,25 +357,82 @@ export const computeFilterRecordGqlOperationFilter = ({ ); } } - case 'CURRENCY': - switch (filter.operand) { - case RecordFilterOperand.GreaterThan: - return { - [correspondingField.name]: { - amountMicros: { gte: parseFloat(filter.value) * 1000000 }, - } as CurrencyFilter, - }; - case RecordFilterOperand.LessThan: - return { - [correspondingField.name]: { - amountMicros: { lte: parseFloat(filter.value) * 1000000 }, - } as CurrencyFilter, - }; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filterType} filter`, - ); + case 'CURRENCY': { + if ( + isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'currencyCode', + subFieldName, + ) + ) { + const parsedCurrencyCodes = JSON.parse(filter.value) as string[]; + + if (parsedCurrencyCodes.length === 0) return undefined; + + const gqlFilter: RecordGqlOperationFilter = { + [correspondingField.name]: { + currencyCode: { in: parsedCurrencyCodes }, + } as CurrencyFilter, + }; + + switch (filter.operand) { + case RecordFilterOperand.Is: + return gqlFilter; + case RecordFilterOperand.IsNot: + return { + not: gqlFilter, + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filterType} / ${subFieldName} filter`, + ); + } + } else if ( + isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'amountMicros', + subFieldName, + ) + ) { + switch (filter.operand) { + case RecordFilterOperand.GreaterThan: + return { + [correspondingField.name]: { + amountMicros: { gte: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }; + case RecordFilterOperand.LessThan: + return { + [correspondingField.name]: { + amountMicros: { lte: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }; + case RecordFilterOperand.Is: + return { + [correspondingField.name]: { + amountMicros: { eq: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }; + case RecordFilterOperand.IsNot: + return { + not: { + [correspondingField.name]: { + amountMicros: { eq: parseFloat(filter.value) * 1000000 }, + } as CurrencyFilter, + }, + }; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filterType} / ${subFieldName} filter`, + ); + } + } else { + throw new Error( + `Unknown subfield ${subFieldName} for ${filterType} filter`, + ); } + } + case 'LINKS': { const linksFilters = generateILikeFiltersForCompositeFields( filter.value, @@ -385,21 +442,21 @@ export const computeFilterRecordGqlOperationFilter = ({ switch (filter.operand) { case RecordFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { or: linksFilters, }; } else { return { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, }, }, }; } case RecordFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { and: linksFilters.map((filter) => { return { @@ -411,7 +468,7 @@ export const computeFilterRecordGqlOperationFilter = ({ return { not: { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, }, }, @@ -432,21 +489,21 @@ export const computeFilterRecordGqlOperationFilter = ({ ); switch (filter.operand) { case RecordFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { or: fullNameFilters, }; } else { return { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, }, }, }; } case RecordFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { and: fullNameFilters.map((filter) => { return { @@ -458,7 +515,7 @@ export const computeFilterRecordGqlOperationFilter = ({ return { not: { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, }, }, @@ -474,7 +531,7 @@ export const computeFilterRecordGqlOperationFilter = ({ case 'ADDRESS': switch (filter.operand) { case RecordFilterOperand.Contains: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { or: [ { @@ -524,14 +581,14 @@ export const computeFilterRecordGqlOperationFilter = ({ } else { return { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, } as AddressFilter, }, }; } case RecordFilterOperand.DoesNotContain: - if (!isCompositeFieldFiter) { + if (!isSubFieldFilter) { return { and: [ { @@ -567,7 +624,7 @@ export const computeFilterRecordGqlOperationFilter = ({ return { not: { [correspondingField.name]: { - [compositeFieldName]: { + [subFieldName]: { ilike: `%${filter.value}%`, } as AddressFilter, }, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts new file mode 100644 index 000000000..b0376be7f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts @@ -0,0 +1,34 @@ +import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType'; +import { CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { assertUnreachable } from 'twenty-shared/utils'; + +export const getDefaultSubFieldNameForCompositeFilterableFieldType = ( + fieldType: FieldType, +): CompositeFieldSubFieldName | undefined => { + if (!isCompositeFilterableFieldType(fieldType as any)) { + return undefined; + } + + const compositeFieldType = fieldType as CompositeFilterableFieldType; + + switch (compositeFieldType) { + case 'CURRENCY': + return 'amountMicros'; + case 'LINKS': + return 'primaryLinkUrl'; + case 'PHONES': + return 'primaryPhoneNumber'; + case 'EMAILS': + return 'primaryEmail'; + case 'ADDRESS': + return 'addressCity'; + case 'ACTOR': + return 'source'; + case 'FULL_NAME': + return 'firstName'; + default: + assertUnreachable(compositeFieldType); + } +}; 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 33ff418ae..f7186e1e4 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 @@ -1,6 +1,9 @@ +import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName'; import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; 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 { FieldMetadataType } from 'twenty-shared/types'; export type GetRecordFilterOperandsParams = { filterType: FilterableFieldType; @@ -21,6 +24,15 @@ type FilterOperandMap = { [K in FilterableFieldType]: readonly RecordFilterOperand[]; }; +// TODO: we would need to refactor the typing of SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS first +// with types like FieldCurrencyValue being derived from a central constant value and not being created like that +// in order to narrow down the possible subfield names for each field type +type CompositeFieldFilterOperandMap = { + [K in FilterableFieldType]: Partial<{ + [S in CompositeFieldSubFieldName]: readonly RecordFilterOperand[]; + }>; +}; + export const FILTER_OPERANDS_MAP = { TEXT: [ RecordFilterOperand.Contains, @@ -113,6 +125,23 @@ export const FILTER_OPERANDS_MAP = { BOOLEAN: [RecordFilterOperand.Is], } as const satisfies FilterOperandMap; +export const COMPOSITE_FIELD_FILTER_OPERANDS_MAP = { + CURRENCY: { + currencyCode: [ + RecordFilterOperand.Is, + RecordFilterOperand.IsNot, + ...emptyOperands, + ], + amountMicros: [ + RecordFilterOperand.GreaterThan, + RecordFilterOperand.LessThan, + RecordFilterOperand.Is, + RecordFilterOperand.IsNot, + ...emptyOperands, + ], + }, +} as const satisfies Partial; + export const getRecordFilterOperands = ({ filterType, subFieldName, @@ -125,7 +154,29 @@ export const getRecordFilterOperands = ({ case 'LINKS': case 'PHONES': return FILTER_OPERANDS_MAP.TEXT; - case 'CURRENCY': + case 'CURRENCY': { + if ( + isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'currencyCode', + subFieldName, + ) + ) { + return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.currencyCode; + } else if ( + isExpectedSubFieldName( + FieldMetadataType.CURRENCY, + 'amountMicros', + subFieldName, + ) + ) { + return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.amountMicros; + } else { + throw new Error( + `Unknown subfield name ${subFieldName} for ${filterType} filter`, + ); + } + } case 'NUMBER': return FILTER_OPERANDS_MAP.NUMBER; case 'RAW_JSON': diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts deleted file mode 100644 index 560886e79..000000000 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeFieldTypeFilterable.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FieldType } from '@/settings/data-model/types/FieldType'; - -type CompositeFilterableFieldType = Extract; - -export const isCompositeFieldTypeSubFieldsFilterable = ( - fieldType: FieldType, -): fieldType is CompositeFilterableFieldType => { - return ( - ['ACTOR', 'FULL_NAME'] satisfies CompositeFilterableFieldType[] - ).includes(fieldType as any); -}; 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/isCompositeTypeFilterableByAnySubField.ts new file mode 100644 index 000000000..deb63ac3b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField.ts @@ -0,0 +1,15 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; + +const COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY = [ + 'ACTOR', + 'CURRENCY', +] satisfies FieldType[]; + +type CompositeTypeNonFilterableWithAny = + (typeof COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY)[number]; + +export const isCompositeTypeFilterableByAnySubField = ( + fieldType: FieldType, +): fieldType is CompositeTypeNonFilterableWithAny => { + return !COMPOSITE_TYPES_NON_FILTERABLE_WITH_ANY.includes(fieldType as any); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isEmptinessOperand.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isEmptinessOperand.ts new file mode 100644 index 000000000..db1b3b03e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isEmptinessOperand.ts @@ -0,0 +1,7 @@ +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; + +export const isEmptinessOperand = (operand: RecordFilterOperand): boolean => { + return [RecordFilterOperand.IsEmpty, RecordFilterOperand.IsNotEmpty].includes( + operand, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts index 2d4719c39..e6d34aeaa 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts @@ -1,36 +1,17 @@ import { CurrencyFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; -export const isMatchingCurrencyFilter = ({ - currencyFilter, - value, -}: { - currencyFilter: CurrencyFilter; - value: number; -}) => { +const isMatchingCurrencyCodeFilter = ( + currencyCodeFilter: CurrencyFilter['currencyCode'], + value: string | null | undefined, +) => { switch (true) { - case currencyFilter.amountMicros?.eq !== undefined: { - return value === currencyFilter.amountMicros.eq; + case currencyCodeFilter?.in !== undefined: { + return isNonEmptyString(value) && currencyCodeFilter.in.includes(value); } - case currencyFilter.amountMicros?.neq !== undefined: { - return value !== currencyFilter.amountMicros.neq; - } - case currencyFilter.amountMicros?.gt !== undefined: { - return value > currencyFilter.amountMicros.gt; - } - case currencyFilter.amountMicros?.gte !== undefined: { - return value >= currencyFilter.amountMicros.gte; - } - case currencyFilter.amountMicros?.lt !== undefined: { - return value < currencyFilter.amountMicros.lt; - } - case currencyFilter.amountMicros?.lte !== undefined: { - return value <= currencyFilter.amountMicros.lte; - } - case currencyFilter.amountMicros?.in !== undefined: { - return currencyFilter.amountMicros.in.includes(value); - } - case currencyFilter.amountMicros?.is !== undefined: { - if (currencyFilter.amountMicros.is === 'NULL') { + case currencyCodeFilter?.is !== undefined: { + if (currencyCodeFilter.is === 'NULL') { return value === null; } else { return value !== null; @@ -38,10 +19,91 @@ export const isMatchingCurrencyFilter = ({ } default: { throw new Error( - `Unexpected amountMicros for currency filter : ${JSON.stringify( - currencyFilter.amountMicros, + `Unexpected operand for currency code filter : ${JSON.stringify( + currencyCodeFilter, )}`, ); } } }; + +const isMatchingAmountMicrosFilter = ( + amountMicrosFilter: CurrencyFilter['amountMicros'], + value: number | null | undefined, +) => { + switch (true) { + case amountMicrosFilter?.eq !== undefined: { + return value === amountMicrosFilter.eq; + } + case amountMicrosFilter?.neq !== undefined: { + return value !== amountMicrosFilter.neq; + } + case amountMicrosFilter?.gt !== undefined: { + return isDefined(value) && value > amountMicrosFilter.gt; + } + case amountMicrosFilter?.gte !== undefined: { + return isDefined(value) && value >= amountMicrosFilter.gte; + } + case amountMicrosFilter?.lt !== undefined: { + return isDefined(value) && value < amountMicrosFilter.lt; + } + case amountMicrosFilter?.lte !== undefined: { + return isDefined(value) && value <= amountMicrosFilter.lte; + } + case amountMicrosFilter?.is !== undefined: { + if (amountMicrosFilter.is === 'NULL') { + return value === null; + } else { + return value !== null; + } + } + default: { + throw new Error( + `Unexpected operand for currency amount micros filter : ${JSON.stringify( + amountMicrosFilter, + )}`, + ); + } + } +}; + +export const isMatchingCurrencyFilter = ({ + currencyFilter, + value, +}: { + currencyFilter: CurrencyFilter; + value: { + amountMicros?: number | null; + currencyCode?: string | null; + }; +}) => { + const shouldMatchCurrencyCodeFilter = isDefined(currencyFilter.currencyCode); + const shouldMatchAmountMicrosFilter = isDefined(currencyFilter.amountMicros); + + if (shouldMatchCurrencyCodeFilter && shouldMatchAmountMicrosFilter) { + return ( + isMatchingAmountMicrosFilter( + currencyFilter.amountMicros, + value.amountMicros, + ) && + isMatchingCurrencyCodeFilter( + currencyFilter.currencyCode, + value.currencyCode, + ) + ); + } else if (shouldMatchAmountMicrosFilter) { + return isMatchingAmountMicrosFilter( + currencyFilter.amountMicros, + value.amountMicros, + ); + } else if (shouldMatchCurrencyCodeFilter) { + return isMatchingCurrencyCodeFilter( + currencyFilter.currencyCode, + value.currencyCode, + ); + } + + throw new Error( + `Unexpected filter for currency : ${JSON.stringify(currencyFilter)}`, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts index 97538adf9..181eab14b 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterConsideredEmpty.ts @@ -8,7 +8,7 @@ export const isRecordFilterConsideredEmpty = ( const { value, operand } = recordFilter; if ( - (!isDefined(value) || value === '') && + (!isDefined(value) || value === '' || value === '[]') && ![ RecordFilterOperand.IsEmpty, RecordFilterOperand.IsNotEmpty, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 83a722dca..c0234e771 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -319,7 +319,7 @@ export const isRecordMatchingFilter = ({ case FieldMetadataType.CURRENCY: { return isMatchingCurrencyFilter({ currencyFilter: filterValue as CurrencyFilter, - value: record[filterKey].amountMicros, + value: record[filterKey], }); } case FieldMetadataType.ACTOR: { diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts index d7f96b4e1..1c41a3ae0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts @@ -11,6 +11,7 @@ import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dro 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 { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; @@ -103,8 +104,14 @@ export const useHandleToggleColumnFilter = ({ const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + const defaultSubFieldName = + getDefaultSubFieldNameForCompositeFilterableFieldType( + fieldMetadataItem.type, + ); + const availableOperandsForFilter = getRecordFilterOperands({ filterType, + subFieldName: defaultSubFieldName, }); const defaultOperand = availableOperandsForFilter[0]; @@ -117,6 +124,7 @@ export const useHandleToggleColumnFilter = ({ label: fieldMetadataItem.label, type: filterType, value: '', + subFieldName: defaultSubFieldName, }; upsertRecordFilter(newFilter); diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts index 76becf223..be0cc9768 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts @@ -1,6 +1,6 @@ import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { RecordFilter, @@ -25,7 +25,7 @@ export const buildValueFromFilter = ({ currentWorkspaceMember?: CurrentWorkspaceMember; label?: string; }) => { - if (isCompositeField(filter.type)) { + if (isCompositeFieldType(filter.type)) { return; } diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx index 29087acbb..dd2734c38 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { Key } from 'ts-key-enum'; import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip'; @@ -57,19 +56,10 @@ export const MultipleSelectDropdown = ({ ); }; - const [itemsInDropdown, setItemInDropdown] = useState([ + const itemsInDropdown = [ ...(filteredSelectedItems ?? []), ...(itemsToSelect ?? []), - ]); - - useEffect(() => { - if (!loadingItems) { - setItemInDropdown([ - ...(filteredSelectedItems ?? []), - ...(itemsToSelect ?? []), - ]); - } - }, [itemsToSelect, filteredSelectedItems, loadingItems]); + ]; useScopedHotkeys( [Key.Escape], diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/Currencies.ts b/packages/twenty-front/src/modules/settings/data-model/constants/Currencies.ts new file mode 100644 index 000000000..25ae9950d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/Currencies.ts @@ -0,0 +1,10 @@ +import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; +import { Currency } from '@/ui/input/components/internal/types/Currency'; + +export const CURRENCIES: Currency[] = Object.entries( + SETTINGS_FIELD_CURRENCY_CODES, +).map(([key, { Icon, label }]) => ({ + value: key, + Icon, + label: `${label} (${key})`, +})); 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 0c60eaea7..ebf2a0803 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 @@ -40,8 +40,8 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { [FieldMetadataType.CURRENCY]: { label: 'Currency', Icon: IllustrationIconCurrency, - subFields: ['amountMicros'], - filterableSubFields: ['amountMicros'], + subFields: ['amountMicros', 'currencyCode'], + filterableSubFields: ['amountMicros', 'currencyCode'], labelBySubField: { amountMicros: 'Amount', currencyCode: 'Currency', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx index deb5829ed..65920c933 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx @@ -4,10 +4,9 @@ import { z } from 'zod'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; -import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; import { Select } from '@/ui/input/components/Select'; -import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; import { useLingui } from '@lingui/react/macro'; import { IconCurrencyDollar } from 'twenty-ui/display'; @@ -24,14 +23,6 @@ type SettingsDataModelFieldCurrencyFormProps = { fieldMetadataItem: Pick; }; -const OPTIONS = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( - ([value, { label, Icon }]) => ({ - label, - value: applySimpleQuotesToString(value), - Icon, - }), -); - export const SettingsDataModelFieldCurrencyForm = ({ disabled, fieldMetadataItem, @@ -67,7 +58,7 @@ export const SettingsDataModelFieldCurrencyForm = ({ onChange={onChange} disabled={disabled} dropdownId="object-field-default-value-select-currency" - options={OPTIONS} + options={CURRENCIES} selectSizeVariant="small" withSearchInput={true} /> diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx index ca32f68bd..6ef890e37 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx @@ -1,5 +1,5 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; @@ -94,7 +94,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({ } LeftIcon={getIcon(field.icon)} text={field.label} - hasSubMenu={isCompositeField(field.type)} + hasSubMenu={isCompositeFieldType(field.type)} /> ))} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx index 243cfb9a8..7e56d99eb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx @@ -1,5 +1,5 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; @@ -48,7 +48,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({ onBack(); }; - if (!isCompositeField(fieldMetadataItem.type)) { + if (!isCompositeFieldType(fieldMetadataItem.type)) { return <>; } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx index 57cf5ddc5..9a80e9eba 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { ReadonlyDeep } from 'type-fest'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent'; import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent'; @@ -40,7 +40,7 @@ export const MatchColumnToFieldSelect = ({ ) => { setSelectedFieldMetadataItem(selectedFieldMetadataItem); - if (!isCompositeField(selectedFieldMetadataItem.type)) { + if (!isCompositeFieldType(selectedFieldMetadataItem.type)) { const correspondingOption = options.find( (option) => option.value === selectedFieldMetadataItem.name, ); @@ -100,11 +100,9 @@ export const MatchColumnToFieldSelect = ({ (option) => option.value === DO_NOT_IMPORT_OPTION_KEY, ); - const shouldDisplaySubFieldMetadataItemSelect = isDefined( - selectedFieldMetadataItem?.type, - ) - ? isCompositeField(selectedFieldMetadataItem?.type) - : false; + const shouldDisplaySubFieldMetadataItemSelect = + isDefined(selectedFieldMetadataItem?.type) && + isCompositeFieldType(selectedFieldMetadataItem?.type); return ( ( - () => - Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( - ([key, { Icon, label }]) => ({ - value: key, - Icon, - label, - }), - ), - [], - ); - - const currency = currencies.find(({ value }) => value === currencyCode); + const currency = CURRENCIES.find(({ value }) => value === currencyCode); useEffect(() => { setInternalText(value); @@ -119,9 +102,8 @@ export const CurrencyInput = ({ return ( {Icon && ( diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx index 874562d2f..fa998caac 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx @@ -7,8 +7,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope'; -import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect'; +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; +import { Currency } from '@/ui/input/components/internal/types/Currency'; import { IconChevronDown } from 'twenty-ui/display'; +import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect'; const StyledDropdownButtonContainer = styled.div` align-items: center; @@ -41,20 +43,12 @@ const StyledIconContainer = styled.div` } `; -export type Currency = { - label: string; - value: string; - Icon: any; -}; - export const CurrencyPickerDropdownButton = ({ - valueCode, + selectedCurrencyCode, onChange, - currencies, }: { - valueCode: string; + selectedCurrencyCode: string; onChange: (currency: Currency) => void; - currencies: Currency[]; }) => { const theme = useTheme(); @@ -67,7 +61,9 @@ export const CurrencyPickerDropdownButton = ({ closeDropdown(); }; - const currency = currencies.find(({ value }) => value === valueCode); + const currency = CURRENCIES.find( + ({ value }) => value === selectedCurrencyCode, + ); const currencyCode = currency?.value ?? CurrencyCode.USD; @@ -85,7 +81,6 @@ export const CurrencyPickerDropdownButton = ({ } dropdownComponents={ diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx index feb25c9bc..de2a5b39a 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx @@ -4,15 +4,15 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { Currency } from './CurrencyPickerDropdownButton'; + +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; +import { Currency } from '@/ui/input/components/internal/types/Currency'; import { MenuItem, MenuItemSelectAvatar } from 'twenty-ui/navigation'; export const CurrencyPickerDropdownSelect = ({ - currencies, selectedCurrency, onChange, }: { - currencies: Currency[]; selectedCurrency?: Currency; onChange: (currency: Currency) => void; }) => { @@ -20,14 +20,14 @@ export const CurrencyPickerDropdownSelect = ({ const filteredCurrencies = useMemo( () => - currencies.filter( + CURRENCIES.filter( ({ value, label }) => value .toLocaleLowerCase() .includes(searchFilter.toLocaleLowerCase()) || label.toLocaleLowerCase().includes(searchFilter.toLocaleLowerCase()), ), - [currencies, searchFilter], + [searchFilter], ); return ( @@ -49,7 +49,7 @@ export const CurrencyPickerDropdownSelect = ({ key={selectedCurrency.value} selected={true} onClick={() => onChange(selectedCurrency)} - text={`${selectedCurrency.label} (${selectedCurrency.value})`} + text={selectedCurrency.label} /> )} {filteredCurrencies.map((item) => diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/types/Currency.ts b/packages/twenty-front/src/modules/ui/input/components/internal/types/Currency.ts new file mode 100644 index 000000000..791c1a8e4 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/types/Currency.ts @@ -0,0 +1,5 @@ +export type Currency = { + label: string; + value: string; + Icon: any; +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index 137422e5f..a4a6606eb 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -1,9 +1,10 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; -import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; -import { isFilterOperandExpectingValue } from '@/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand'; +import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty'; import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { isNonEmptyString } from '@sniptt/guards'; @@ -27,11 +28,12 @@ export const EditableFilterChip = ({ const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon); const operandLabelShort = getOperandLabelShort(recordFilter.operand); + const operandIsEmptiness = isEmptinessOperand(recordFilter.operand); const recordFilterSubFieldName = recordFilter.subFieldName; const subFieldLabel = - isCompositeField(fieldMetadataItem.type) && + isCompositeFieldType(fieldMetadataItem.type) && isNonEmptyString(recordFilterSubFieldName) && isValidSubFieldName(recordFilterSubFieldName) ? getCompositeSubFieldLabel( @@ -44,11 +46,9 @@ export const EditableFilterChip = ({ ? `${recordFilter.label} / ${subFieldLabel}` : recordFilter.label; - const shouldDisplayOperandLabelShort = - isNonEmptyString(recordFilter.value) || - !isFilterOperandExpectingValue(recordFilter.operand); + const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter); - const labelKey = `${fieldNameLabel}${shouldDisplayOperandLabelShort ? operandLabelShort : ''}`; + const labelKey = `${fieldNameLabel}${!operandIsEmptiness && !recordFilterIsEmpty ? operandLabelShort : operandIsEmptiness ? ` ${operandLabelShort}` : ''}`; return (