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:
@ -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} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -66,6 +66,7 @@ export type DateFilter = {
|
||||
|
||||
export type CurrencyFilter = {
|
||||
amountMicros?: FloatFilter;
|
||||
currencyCode?: SelectFilter;
|
||||
};
|
||||
|
||||
export type URLFilter = {
|
||||
|
||||
@ -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 />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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] =
|
||||
@ -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 />
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
@ -1 +1 @@
|
||||
export const NUMBER_FILTER_TYPES = ['NUMBER', 'CURRENCY', 'PHONES'];
|
||||
export const NUMBER_FILTER_TYPES = ['NUMBER', 'PHONES'];
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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',
|
||||
};
|
||||
@ -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
|
||||
>;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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':
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
@ -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)}`,
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ export const isRecordFilterConsideredEmpty = (
|
||||
const { value, operand } = recordFilter;
|
||||
|
||||
if (
|
||||
(!isDefined(value) || value === '') &&
|
||||
(!isDefined(value) || value === '' || value === '[]') &&
|
||||
![
|
||||
RecordFilterOperand.IsEmpty,
|
||||
RecordFilterOperand.IsNotEmpty,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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})`,
|
||||
}));
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 <></>;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export type Currency = {
|
||||
label: string;
|
||||
value: string;
|
||||
Icon: any;
|
||||
};
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user