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);
- }}
- >
-
+ )}
{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 (