Implement sub-field filtering on CURRENCY field type (#11726)

This PR implements sub-field filtering on CURRENCY field type and
improves many related zones.

- Created a ObjectFilterDropdownCurrencySelect dropdown component for
filtering on multiple currencies
- Added currencyCode sub-field to CurrencyFilter type
- Created getDefaultSubFieldNameForCompositeFilterableFieldType to avoid
situation where we don't have any sub field name in sub field filtering
situations.
- Implemented filtering for currencyCode in
computeFilterRecordGqlOperationFilter
- Implemented CURRENCY type in getRecordFilterOperands
- Implemented isMatchingCurrencyFilter for using in
isRecordMatchingFilter for proper optimistic rendering
- Created turnCurrencyIntoSelectableItem to help
ObjectFilterDropdownCurrencySelect

Testing : 
- Added test for currency sub fields in getOperandsForFilterType
- Completely reworked isMatchingCurrencyFilter test

Improvements : 
- Created a unique CURRENCIES constant to avoid re-creating it at
various places
- Derive the type FilterableFieldType from a constant array
FILTERABLE_FIELD_TYPES, so it's easier to work with
- Added areCompositeTypeSubFieldsFilterable
- Fixed a bug with empty value '[]' that was preventing the auto-removal
of a filter chip

Miscellaneous : 
- Created isExpectedSubFieldName util to do a type-safe check of a
subFieldName
- Better naming : renamed isCompositeField to isCompositeFieldType
- Created isCompositeTypeFilterableWithAny to specify which field types
are filterable by any sub field
- Better naming : renamed
ObjectFilterDropdownFilterSelectCompositeFieldSubMenu to
ObjectFilterDropdownSubFieldSelect
- Better naming : renamed ObjectFilterDropdownFilterSelect to
ObjectFilterDropdownFieldSelect
- Created isEmptinessOperand util instead of duplicating the same
hard-coded check in multiple places
- Better naming : used subFieldName instead of compositeFieldName for
consistency
- UseEffect removal : removed unnecessary useEffect in
MultipleSelectDropdown

Fixes a bug where Empty and Not weren't appearing in filter chip in
particular cases
Fixes https://github.com/twentyhq/core-team-issues/issues/498
Fixes https://github.com/twentyhq/twenty/issues/7558
This commit is contained in:
Lucas Bordeau
2025-04-25 19:33:00 +02:00
committed by GitHub
parent f201091c68
commit 50cb32d122
51 changed files with 1358 additions and 422 deletions

View File

@ -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' && <ObjectFilterDropdownBooleanSelect />}
{filterType === 'CURRENCY' &&
(isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'currencyCode',
recordFilter.subFieldName,
) ? (
<>
<ObjectFilterDropdownCurrencySelect dropdownWidth={280} />
</>
) : (
<></>
))}
</>
);
};

View File

@ -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(

View File

@ -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);

View File

@ -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}
>
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
focused={selectedItemId === '-1'}
onClick={() => {
{compositeFieldTypeIsFilterableByAnySubField && (
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
</SelectableListItem>
>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
focused={selectedItemId === '-1'}
onClick={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
</SelectableListItem>
)}
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
subFieldNames.map((subFieldName, index) => (
<SelectableListItem
itemId={subFieldName}
key={`select-filter-${index}`}
@ -153,16 +163,21 @@ export const AdvancedFilterSubFieldSelectMenu = ({
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
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,
)}
/>
</SelectableListItem>
))}

View File

@ -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 (
<StyledValueDropdownContainer>
{operandHasNoInput ? (
@ -68,9 +80,7 @@ export const AdvancedFilterValueInput = ({
<AdvancedFilterValueInputDropdownButtonClickableSelect
recordFilterId={recordFilterId}
/>
) : isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType)) ? (
) : showFilterTextInput ? (
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
) : (
<Dropdown

View File

@ -66,6 +66,7 @@ export type DateFilter = {
export type CurrencyFilter = {
amountMicros?: FloatFilter;
currencyCode?: SelectFilter;
};
export type URLFilter = {

View File

@ -1,9 +1,9 @@
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
import { ObjectFilterDropdownSubFieldSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect';
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
import { ObjectFilterDropdownFieldSelect } from './ObjectFilterDropdownFieldSelect';
type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string;
@ -35,9 +35,9 @@ export const MultipleFiltersDropdownContent = ({
filterDropdownId={filterDropdownId}
/>
) : shouldShowCompositeSelectionSubMenu ? (
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
<ObjectFilterDropdownSubFieldSelect />
) : (
<ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
<ObjectFilterDropdownFieldSelect isAdvancedFilterButtonVisible />
)}
</>
);

View File

@ -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 (
<>
<DropdownMenuSearchInput
autoFocus
type="text"
value={searchText}
placeholder={t`Search currency`}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value);
}}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight width={dropdownWidth ?? 200}>
{filteredSelectedItems?.map((item) => {
return (
<MenuItemMultiSelectAvatar
key={item.id}
selected={true}
onSelectChange={(newCheckedValue) => {
handleMultipleItemSelectChange(item, newCheckedValue);
}}
avatar={
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={item.name}
avatarUrl={item.avatarUrl}
LeftIcon={item.AvatarIcon}
avatarType={item.avatarType}
isIconInverted={item.isIconInverted}
placeholderColorSeed={item.id}
/>
}
/>
);
})}
{filteredSelectableItems?.map((item) => {
return (
<MenuItemMultiSelectAvatar
key={item.id}
selected={false}
onSelectChange={(newCheckedValue) => {
handleMultipleItemSelectChange(item, newCheckedValue);
}}
avatar={
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={item.name}
avatarUrl={item.avatarUrl}
LeftIcon={item.AvatarIcon}
avatarType={item.avatarType}
isIconInverted={item.isIconInverted}
placeholderColorSeed={item.id}
/>
}
/>
);
})}
{showNoResult && <MenuItem text={t`No results`} />}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -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] =

View File

@ -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 = ({
<ObjectFilterDropdownTextInput />
</>
))}
{filterType === 'CURRENCY' &&
(isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'currencyCode',
subFieldNameUsedInDropdown,
) ? (
<>
<ObjectFilterDropdownCurrencySelect />
</>
) : isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'amountMicros',
subFieldNameUsedInDropdown,
) ? (
<>
<ObjectFilterDropdownNumberInput />
</>
) : (
<></>
))}
{['SELECT', 'MULTI_SELECT'].includes(filterType) && (
<>
<ObjectFilterDropdownSearchInput />

View File

@ -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);

View File

@ -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();

View File

@ -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,
});
}}
/>

View File

@ -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)}
</DropdownMenuHeader>
{/* <StyledInput
<StyledInput
value={searchText}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value)
}
/> */}
/>
<DropdownMenuItemsContainer>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={['-1', ...options]}
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
>
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
focused={selectedItemId === '-1'}
{compositeFieldTypeFilterableByAnySubField ? (
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(
objectFilterDropdownSubMenuFieldType,
)} field`}
/>
</SelectableListItem>
>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
</SelectableListItem>
) : (
<></>
)}
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
<SelectableListItem
@ -233,7 +240,10 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
LeftIcon={getIcon(
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
fieldMetadataItemUsedInDropdown?.icon,
)}
/>
</SelectableListItem>
))}

View File

@ -1 +1 @@
export const NUMBER_FILTER_TYPES = ['NUMBER', 'CURRENCY', 'PHONES'];
export const NUMBER_FILTER_TYPES = ['NUMBER', 'PHONES'];

View File

@ -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,
});
}

View File

@ -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);
});

View File

@ -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);

View File

@ -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);

View File

@ -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
);
};

View File

@ -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,
});

View File

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

View File

@ -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,
];
}, []);

View File

@ -0,0 +1,8 @@
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
export const ICON_NAME_BY_SUB_FIELD: Partial<
Record<CompositeFieldSubFieldName, string>
> = {
currencyCode: 'IconCurrencyDollar',
amountMicros: 'IconNumber95Small',
};

View File

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

View File

@ -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,

View File

@ -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}',
);
});
});

View File

@ -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);
};

View File

@ -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,
},

View File

@ -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);
}
};

View File

@ -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<CompositeFieldFilterOperandMap>;
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':

View File

@ -1,11 +0,0 @@
import { FieldType } from '@/settings/data-model/types/FieldType';
type CompositeFilterableFieldType = Extract<FieldType, 'ACTOR' | 'FULL_NAME'>;
export const isCompositeFieldTypeSubFieldsFilterable = (
fieldType: FieldType,
): fieldType is CompositeFilterableFieldType => {
return (
['ACTOR', 'FULL_NAME'] satisfies CompositeFilterableFieldType[]
).includes(fieldType as any);
};

View File

@ -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);
};

View File

@ -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,
);
};

View File

@ -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)}`,
);
};

View File

@ -8,7 +8,7 @@ export const isRecordFilterConsideredEmpty = (
const { value, operand } = recordFilter;
if (
(!isDefined(value) || value === '') &&
(!isDefined(value) || value === '' || value === '[]') &&
![
RecordFilterOperand.IsEmpty,
RecordFilterOperand.IsNotEmpty,

View File

@ -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: {

View File

@ -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);

View File

@ -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;
}

View File

@ -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],

View File

@ -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})`,
}));

View File

@ -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',

View File

@ -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<FieldMetadataItem, 'defaultValue'>;
};
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}
/>

View File

@ -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)}
/>
))}
</DropdownMenuItemsContainer>

View File

@ -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 <></>;
}

View File

@ -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 (
<Dropdown

View File

@ -1,10 +1,11 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton';
import { Currency } from '@/ui/input/components/internal/types/Currency';
import { IMaskInput } from 'react-imask';
import { IconComponent } from 'twenty-ui/display';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
@ -50,12 +51,6 @@ export type CurrencyInputProps = {
hotkeyScope: string;
};
type Currency = {
label: string;
value: string;
Icon: any;
};
export const CurrencyInput = ({
autoFocus,
value,
@ -96,19 +91,7 @@ export const CurrencyInput = ({
hotkeyScope,
});
const currencies = useMemo<Currency[]>(
() =>
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 (
<StyledContainer ref={wrapperRef}>
<CurrencyPickerDropdownButton
valueCode={currency?.value ?? ''}
selectedCurrencyCode={currency?.value ?? ''}
onChange={handleCurrencyChange}
currencies={currencies}
/>
<StyledIcon>
{Icon && (

View File

@ -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={
<CurrencyPickerDropdownSelect
currencies={currencies}
selectedCurrency={currency}
onChange={handleChange}
/>

View File

@ -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) =>

View File

@ -0,0 +1,5 @@
export type Currency = {
label: string;
value: string;
Icon: any;
};

View File

@ -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 (
<SortOrFilterChip