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 { AdvancedFilterDropdownDateInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput';
|
||||||
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
|
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 { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
|
||||||
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
||||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
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 { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
|
||||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
type AdvancedFilterDropdownFilterInputProps = {
|
type AdvancedFilterDropdownFilterInputProps = {
|
||||||
filterDropdownId?: string;
|
filterDropdownId?: string;
|
||||||
@ -65,6 +68,18 @@ export const AdvancedFilterDropdownFilterInput = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{filterType === 'BOOLEAN' && <ObjectFilterDropdownBooleanSelect />}
|
{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 { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
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 { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
|
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
|
||||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||||
@ -38,7 +38,7 @@ export const AdvancedFilterFieldSelectDropdownButtonClickableSelect = ({
|
|||||||
|
|
||||||
const subFieldLabel =
|
const subFieldLabel =
|
||||||
isDefined(fieldMetadataItem) &&
|
isDefined(fieldMetadataItem) &&
|
||||||
isCompositeField(fieldMetadataItem.type) &&
|
isCompositeFieldType(fieldMetadataItem.type) &&
|
||||||
isNonEmptyString(recordFilter?.subFieldName) &&
|
isNonEmptyString(recordFilter?.subFieldName) &&
|
||||||
isValidSubFieldName(recordFilter.subFieldName)
|
isValidSubFieldName(recordFilter.subFieldName)
|
||||||
? getCompositeSubFieldLabel(
|
? getCompositeSubFieldLabel(
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { ObjectFilterDropdownFilterSelectMenuItemV2 } from '@/object-record/obje
|
|||||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
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 { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
@ -103,7 +103,7 @@ export const AdvancedFilterFieldSelectMenu = ({
|
|||||||
selectedFieldMetadataItem.type,
|
selectedFieldMetadataItem.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isCompositeField(filterType)) {
|
if (isCompositeFieldType(filterType)) {
|
||||||
setObjectFilterDropdownSubMenuFieldType(filterType);
|
setObjectFilterDropdownSubMenuFieldType(filterType);
|
||||||
|
|
||||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||||
|
|||||||
@ -11,7 +11,9 @@ import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/o
|
|||||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||||
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
|
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 { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
@ -87,19 +89,23 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
|
const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
|
||||||
objectFilterDropdownSubMenuFieldType
|
objectFilterDropdownSubMenuFieldType
|
||||||
].filterableSubFields.sort((a, b) => a.localeCompare(b));
|
].filterableSubFields.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
const subFieldsAreFilterable =
|
const subFieldsAreFilterable =
|
||||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||||
isCompositeFieldTypeSubFieldsFilterable(
|
areCompositeTypeSubFieldsFilterable(fieldMetadataItemUsedInDropdown.type);
|
||||||
|
|
||||||
|
const compositeFieldTypeIsFilterableByAnySubField =
|
||||||
|
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||||
|
isCompositeTypeFilterableByAnySubField(
|
||||||
fieldMetadataItemUsedInDropdown.type,
|
fieldMetadataItemUsedInDropdown.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectableItemIdArray = [
|
const selectableItemIdArray = [
|
||||||
'-1',
|
'-1',
|
||||||
...options.map((subFieldName) => subFieldName),
|
...subFieldNames.map((subFieldName) => subFieldName),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -120,24 +126,28 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
|||||||
selectableItemIdArray={selectableItemIdArray}
|
selectableItemIdArray={selectableItemIdArray}
|
||||||
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
||||||
>
|
>
|
||||||
<SelectableListItem
|
{compositeFieldTypeIsFilterableByAnySubField && (
|
||||||
itemId={'-1'}
|
<SelectableListItem
|
||||||
key={`select-filter-${-1}`}
|
itemId={'-1'}
|
||||||
onEnter={() => {
|
key={`select-filter-${-1}`}
|
||||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
onEnter={() => {
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
focused={selectedItemId === '-1'}
|
|
||||||
onClick={() => {
|
|
||||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||||
}}
|
}}
|
||||||
LeftIcon={IconApps}
|
>
|
||||||
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
|
<MenuItem
|
||||||
/>
|
key={`select-filter-${-1}`}
|
||||||
</SelectableListItem>
|
testId={`select-filter-${-1}`}
|
||||||
|
focused={selectedItemId === '-1'}
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||||
|
}}
|
||||||
|
LeftIcon={IconApps}
|
||||||
|
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
|
||||||
|
/>
|
||||||
|
</SelectableListItem>
|
||||||
|
)}
|
||||||
{subFieldsAreFilterable &&
|
{subFieldsAreFilterable &&
|
||||||
options.map((subFieldName, index) => (
|
subFieldNames.map((subFieldName, index) => (
|
||||||
<SelectableListItem
|
<SelectableListItem
|
||||||
itemId={subFieldName}
|
itemId={subFieldName}
|
||||||
key={`select-filter-${index}`}
|
key={`select-filter-${index}`}
|
||||||
@ -153,16 +163,21 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
|||||||
key={`select-filter-${index}`}
|
key={`select-filter-${index}`}
|
||||||
testId={`select-filter-${index}`}
|
testId={`select-filter-${index}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectFilter(
|
if (isDefined(fieldMetadataItemUsedInDropdown)) {
|
||||||
fieldMetadataItemUsedInDropdown,
|
handleSelectFilter(
|
||||||
subFieldName,
|
fieldMetadataItemUsedInDropdown,
|
||||||
);
|
subFieldName,
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
text={getCompositeSubFieldLabel(
|
text={getCompositeSubFieldLabel(
|
||||||
objectFilterDropdownSubMenuFieldType,
|
objectFilterDropdownSubMenuFieldType,
|
||||||
subFieldName,
|
subFieldName,
|
||||||
)}
|
)}
|
||||||
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
|
LeftIcon={getIcon(
|
||||||
|
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
|
||||||
|
fieldMetadataItemUsedInDropdown?.icon,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</SelectableListItem>
|
</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 { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
|
||||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
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 { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
const StyledValueDropdownContainer = styled.div`
|
const StyledValueDropdownContainer = styled.div`
|
||||||
@ -60,6 +62,16 @@ export const AdvancedFilterValueInput = ({
|
|||||||
? ({ y: -33, x: 0 } satisfies DropdownOffset)
|
? ({ y: -33, x: 0 } satisfies DropdownOffset)
|
||||||
: DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET;
|
: 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 (
|
return (
|
||||||
<StyledValueDropdownContainer>
|
<StyledValueDropdownContainer>
|
||||||
{operandHasNoInput ? (
|
{operandHasNoInput ? (
|
||||||
@ -68,9 +80,7 @@ export const AdvancedFilterValueInput = ({
|
|||||||
<AdvancedFilterValueInputDropdownButtonClickableSelect
|
<AdvancedFilterValueInputDropdownButtonClickableSelect
|
||||||
recordFilterId={recordFilterId}
|
recordFilterId={recordFilterId}
|
||||||
/>
|
/>
|
||||||
) : isDefined(filterType) &&
|
) : showFilterTextInput ? (
|
||||||
(TEXT_FILTER_TYPES.includes(filterType) ||
|
|
||||||
NUMBER_FILTER_TYPES.includes(filterType)) ? (
|
|
||||||
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
|
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export type DateFilter = {
|
|||||||
|
|
||||||
export type CurrencyFilter = {
|
export type CurrencyFilter = {
|
||||||
amountMicros?: FloatFilter;
|
amountMicros?: FloatFilter;
|
||||||
|
currencyCode?: SelectFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type URLFilter = {
|
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 { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
|
import { ObjectFilterDropdownFieldSelect } from './ObjectFilterDropdownFieldSelect';
|
||||||
|
|
||||||
type MultipleFiltersDropdownContentProps = {
|
type MultipleFiltersDropdownContentProps = {
|
||||||
filterDropdownId?: string;
|
filterDropdownId?: string;
|
||||||
@ -35,9 +35,9 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
filterDropdownId={filterDropdownId}
|
filterDropdownId={filterDropdownId}
|
||||||
/>
|
/>
|
||||||
) : shouldShowCompositeSelectionSubMenu ? (
|
) : 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;
|
isAdvancedFilterButtonVisible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ObjectFilterDropdownFilterSelect = ({
|
export const ObjectFilterDropdownFieldSelect = ({
|
||||||
isAdvancedFilterButtonVisible,
|
isAdvancedFilterButtonVisible,
|
||||||
}: ObjectFilterDropdownFilterSelectProps) => {
|
}: ObjectFilterDropdownFieldSelectProps) => {
|
||||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] =
|
const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] =
|
||||||
@ -10,6 +10,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
|||||||
|
|
||||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
|
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 { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
|
||||||
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
||||||
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
|
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 { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
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 { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
type ObjectFilterDropdownFilterInputProps = {
|
type ObjectFilterDropdownFilterInputProps = {
|
||||||
@ -105,6 +108,26 @@ export const ObjectFilterDropdownFilterInput = ({
|
|||||||
<ObjectFilterDropdownTextInput />
|
<ObjectFilterDropdownTextInput />
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
{filterType === 'CURRENCY' &&
|
||||||
|
(isExpectedSubFieldName(
|
||||||
|
FieldMetadataType.CURRENCY,
|
||||||
|
'currencyCode',
|
||||||
|
subFieldNameUsedInDropdown,
|
||||||
|
) ? (
|
||||||
|
<>
|
||||||
|
<ObjectFilterDropdownCurrencySelect />
|
||||||
|
</>
|
||||||
|
) : isExpectedSubFieldName(
|
||||||
|
FieldMetadataType.CURRENCY,
|
||||||
|
'amountMicros',
|
||||||
|
subFieldNameUsedInDropdown,
|
||||||
|
) ? (
|
||||||
|
<>
|
||||||
|
<ObjectFilterDropdownNumberInput />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
))}
|
||||||
{['SELECT', 'MULTI_SELECT'].includes(filterType) && (
|
{['SELECT', 'MULTI_SELECT'].includes(filterType) && (
|
||||||
<>
|
<>
|
||||||
<ObjectFilterDropdownSearchInput />
|
<ObjectFilterDropdownSearchInput />
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object-
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
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 { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||||
@ -113,7 +113,9 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
|
|
||||||
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
||||||
|
|
||||||
const shouldShowSubMenu = isCompositeField(fieldMetadataItemToSelect.type);
|
const shouldShowSubMenu = isCompositeFieldType(
|
||||||
|
fieldMetadataItemToSelect.type,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
@ -122,7 +124,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
fieldMetadataItemToSelect.type,
|
fieldMetadataItemToSelect.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isCompositeField(filterType)) {
|
if (isCompositeFieldType(filterType)) {
|
||||||
setObjectFilterDropdownSubMenuFieldType(filterType);
|
setObjectFilterDropdownSubMenuFieldType(filterType);
|
||||||
|
|
||||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemToSelect.id);
|
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemToSelect.id);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
@ -28,7 +28,9 @@ export const ObjectFilterDropdownFilterSelectMenuItemV2 = ({
|
|||||||
|
|
||||||
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
||||||
|
|
||||||
const shouldShowSubMenu = isCompositeField(fieldMetadataItemToSelect.type);
|
const shouldShowSubMenu = isCompositeFieldType(
|
||||||
|
fieldMetadataItemToSelect.type,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM
|
|||||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
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 { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
@ -23,6 +24,10 @@ export const ObjectFilterDropdownNumberInput = () => {
|
|||||||
selectedFilterComponentState,
|
selectedFilterComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
|
||||||
|
subFieldNameUsedInDropdownComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { applyRecordFilter } = useApplyRecordFilter();
|
const { applyRecordFilter } = useApplyRecordFilter();
|
||||||
|
|
||||||
const [hasFocused, setHasFocused] = useState(false);
|
const [hasFocused, setHasFocused] = useState(false);
|
||||||
@ -70,7 +75,7 @@ export const ObjectFilterDropdownNumberInput = () => {
|
|||||||
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
||||||
positionInRecordFilterGroup:
|
positionInRecordFilterGroup:
|
||||||
selectedFilter?.positionInRecordFilterGroup,
|
selectedFilter?.positionInRecordFilterGroup,
|
||||||
subFieldName: selectedFilter?.subFieldName,
|
subFieldName: subFieldNameUsedInDropdown,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
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 { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
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 { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||||
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
|
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 { 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 { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
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 { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
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 { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
export const ObjectFilterDropdownSubFieldSelect = () => {
|
||||||
const [searchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
@ -154,7 +157,11 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
|||||||
|
|
||||||
const subFieldsAreFilterable =
|
const subFieldsAreFilterable =
|
||||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||||
isCompositeFieldTypeSubFieldsFilterable(
|
areCompositeTypeSubFieldsFilterable(fieldMetadataItemUsedInDropdown.type);
|
||||||
|
|
||||||
|
const compositeFieldTypeFilterableByAnySubField =
|
||||||
|
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||||
|
isCompositeTypeFilterableByAnySubField(
|
||||||
fieldMetadataItemUsedInDropdown.type,
|
fieldMetadataItemUsedInDropdown.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -170,41 +177,41 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
|||||||
>
|
>
|
||||||
{getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)}
|
{getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)}
|
||||||
</DropdownMenuHeader>
|
</DropdownMenuHeader>
|
||||||
{/* <StyledInput
|
<StyledInput
|
||||||
value={searchText}
|
value={searchText}
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="Search fields"
|
placeholder="Search fields"
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setSearchText(event.target.value)
|
setSearchText(event.target.value)
|
||||||
}
|
}
|
||||||
/> */}
|
/>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
||||||
selectableItemIdArray={['-1', ...options]}
|
selectableItemIdArray={['-1', ...options]}
|
||||||
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
|
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
|
||||||
>
|
>
|
||||||
<SelectableListItem
|
{compositeFieldTypeFilterableByAnySubField ? (
|
||||||
itemId={'-1'}
|
<SelectableListItem
|
||||||
key={`select-filter-${-1}`}
|
itemId={'-1'}
|
||||||
onEnter={() => {
|
|
||||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
focused={selectedItemId === '-1'}
|
|
||||||
key={`select-filter-${-1}`}
|
key={`select-filter-${-1}`}
|
||||||
testId={`select-filter-${-1}`}
|
onEnter={() => {
|
||||||
onClick={() => {
|
|
||||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||||
}}
|
}}
|
||||||
LeftIcon={IconApps}
|
>
|
||||||
text={`Any ${getFilterableFieldTypeLabel(
|
<MenuItem
|
||||||
objectFilterDropdownSubMenuFieldType,
|
key={`select-filter-${-1}`}
|
||||||
)} field`}
|
testId={`select-filter-${-1}`}
|
||||||
/>
|
onClick={() => {
|
||||||
</SelectableListItem>
|
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||||
|
}}
|
||||||
|
LeftIcon={IconApps}
|
||||||
|
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
|
||||||
|
/>
|
||||||
|
</SelectableListItem>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{subFieldsAreFilterable &&
|
{subFieldsAreFilterable &&
|
||||||
options.map((subFieldName, index) => (
|
options.map((subFieldName, index) => (
|
||||||
<SelectableListItem
|
<SelectableListItem
|
||||||
@ -233,7 +240,10 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
|||||||
objectFilterDropdownSubMenuFieldType,
|
objectFilterDropdownSubMenuFieldType,
|
||||||
subFieldName,
|
subFieldName,
|
||||||
)}
|
)}
|
||||||
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
|
LeftIcon={getIcon(
|
||||||
|
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
|
||||||
|
fieldMetadataItemUsedInDropdown?.icon,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</SelectableListItem>
|
</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 { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
type SelectFilterParams = {
|
type SelectFilterParams = {
|
||||||
fieldMetadataItemId: string;
|
fieldMetadataItemId: string;
|
||||||
@ -59,8 +60,12 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => {
|
|||||||
|
|
||||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||||
|
|
||||||
|
const defaultSubFieldName =
|
||||||
|
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
|
||||||
|
|
||||||
const firstOperand = getRecordFilterOperands({
|
const firstOperand = getRecordFilterOperands({
|
||||||
filterType,
|
filterType,
|
||||||
|
subFieldName: defaultSubFieldName,
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
setSelectedOperandInDropdown(firstOperand);
|
setSelectedOperandInDropdown(firstOperand);
|
||||||
@ -79,6 +84,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => {
|
|||||||
value,
|
value,
|
||||||
type: filterType,
|
type: filterType,
|
||||||
label: fieldMetadataItem.label,
|
label: fieldMetadataItem.label,
|
||||||
|
subFieldName: defaultSubFieldName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
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', () => {
|
describe('getOperandsForFilterType', () => {
|
||||||
const emptyOperands = [
|
const emptyOperands = [
|
||||||
@ -18,6 +20,18 @@ describe('getOperandsForFilterType', () => {
|
|||||||
RecordFilterOperand.LessThan,
|
RecordFilterOperand.LessThan,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const currencyAmountMicrosOperands = [
|
||||||
|
RecordFilterOperand.GreaterThan,
|
||||||
|
RecordFilterOperand.LessThan,
|
||||||
|
RecordFilterOperand.Is,
|
||||||
|
RecordFilterOperand.IsNot,
|
||||||
|
];
|
||||||
|
|
||||||
|
const currencyCurrencyCodeOperands = [
|
||||||
|
RecordFilterOperand.Is,
|
||||||
|
RecordFilterOperand.IsNot,
|
||||||
|
];
|
||||||
|
|
||||||
const dateOperands = [
|
const dateOperands = [
|
||||||
RecordFilterOperand.Is,
|
RecordFilterOperand.Is,
|
||||||
RecordFilterOperand.IsRelative,
|
RecordFilterOperand.IsRelative,
|
||||||
@ -36,7 +50,16 @@ describe('getOperandsForFilterType', () => {
|
|||||||
['ADDRESS', [...containsOperands, ...emptyOperands]],
|
['ADDRESS', [...containsOperands, ...emptyOperands]],
|
||||||
['LINKS', [...containsOperands, ...emptyOperands]],
|
['LINKS', [...containsOperands, ...emptyOperands]],
|
||||||
['ACTOR', [...containsOperands, ...emptyOperands]],
|
['ACTOR', [...containsOperands, ...emptyOperands]],
|
||||||
['CURRENCY', [...numberOperands, ...emptyOperands]],
|
[
|
||||||
|
'CURRENCY',
|
||||||
|
[...currencyCurrencyCodeOperands, ...emptyOperands],
|
||||||
|
'currencyCode',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'CURRENCY',
|
||||||
|
[...currencyAmountMicrosOperands, ...emptyOperands],
|
||||||
|
'amountMicros',
|
||||||
|
],
|
||||||
['NUMBER', [...numberOperands, ...emptyOperands]],
|
['NUMBER', [...numberOperands, ...emptyOperands]],
|
||||||
['DATE', [...dateOperands, ...emptyOperands]],
|
['DATE', [...dateOperands, ...emptyOperands]],
|
||||||
['DATE_TIME', [...dateOperands, ...emptyOperands]],
|
['DATE_TIME', [...dateOperands, ...emptyOperands]],
|
||||||
@ -44,12 +67,20 @@ describe('getOperandsForFilterType', () => {
|
|||||||
[undefined, []],
|
[undefined, []],
|
||||||
[null, []],
|
[null, []],
|
||||||
['UNKNOWN_TYPE', []],
|
['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}`, () => {
|
it(`should return correct operands for FilterType.${filterType}`, () => {
|
||||||
const result = getRecordFilterOperands({
|
const result = getRecordFilterOperands({
|
||||||
filterType: filterType as FilterableFieldType,
|
filterType: filterType as FilterableFieldType,
|
||||||
|
subFieldName,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(expectedOperands);
|
expect(result).toEqual(expectedOperands);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,5 +4,6 @@ import {
|
|||||||
} from '@/settings/data-model/types/CompositeFieldType';
|
} from '@/settings/data-model/types/CompositeFieldType';
|
||||||
import { FieldType } from '@/settings/data-model/types/FieldType';
|
import { FieldType } from '@/settings/data-model/types/FieldType';
|
||||||
|
|
||||||
export const isCompositeField = (type: FieldType): type is CompositeFieldType =>
|
export const isCompositeFieldType = (
|
||||||
COMPOSITE_FIELD_TYPES.includes(type as any);
|
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 { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||||
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
|
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
|
||||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
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 { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
||||||
import { FormFieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
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 { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { IconCircleOff } from 'twenty-ui/display';
|
import { IconCircleOff } from 'twenty-ui/display';
|
||||||
@ -26,21 +26,13 @@ export const FormCurrencyFieldInput = ({
|
|||||||
readonly,
|
readonly,
|
||||||
}: FormCurrencyFieldInputProps) => {
|
}: FormCurrencyFieldInputProps) => {
|
||||||
const currencies = useMemo(() => {
|
const currencies = useMemo(() => {
|
||||||
const currencies = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
|
||||||
([key, { Icon, label }]) => ({
|
|
||||||
value: key,
|
|
||||||
Icon,
|
|
||||||
label: `${label} (${key})`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'No currency',
|
label: 'No currency',
|
||||||
value: '',
|
value: '',
|
||||||
Icon: IconCircleOff,
|
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 { FieldType } from '@/settings/data-model/types/FieldType';
|
||||||
import { PickLiteral } from '~/types/PickLiteral';
|
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<
|
export type FilterableFieldType = PickLiteral<
|
||||||
FieldType,
|
FieldType,
|
||||||
| 'TEXT'
|
FilterableFieldTypeBaseLiteral
|
||||||
| 'PHONES'
|
|
||||||
| 'EMAILS'
|
|
||||||
| 'DATE_TIME'
|
|
||||||
| 'DATE'
|
|
||||||
| 'NUMBER'
|
|
||||||
| 'CURRENCY'
|
|
||||||
| 'FULL_NAME'
|
|
||||||
| 'LINKS'
|
|
||||||
| 'RELATION'
|
|
||||||
| 'ADDRESS'
|
|
||||||
| 'SELECT'
|
|
||||||
| 'RATING'
|
|
||||||
| 'MULTI_SELECT'
|
|
||||||
| 'ACTOR'
|
|
||||||
| 'ARRAY'
|
|
||||||
| 'RAW_JSON'
|
|
||||||
| 'BOOLEAN'
|
|
||||||
>;
|
>;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||||
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
|
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', () => {
|
it('select field type with empty options', () => {
|
||||||
const selectFieldMetadata = companyMockObjectMetadataItem.fields.find(
|
const selectFieldMetadata = companyMockObjectMetadataItem.fields.find(
|
||||||
(field) => field.type === FieldMetadataType.SELECT,
|
(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';
|
import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter';
|
||||||
|
|
||||||
describe('isMatchingCurrencyFilter', () => {
|
describe('isMatchingCurrencyFilter', () => {
|
||||||
describe('eq', () => {
|
describe('amountMicros', () => {
|
||||||
it('value equals eq filter', () => {
|
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 = {
|
const currencyFilter: CurrencyFilter = {
|
||||||
amountMicros: { eq: 10 },
|
amountMicros: { eq: 10 },
|
||||||
};
|
currencyCode: { in: ['USD'] },
|
||||||
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' },
|
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
isMatchingCurrencyFilter({ currencyFilter, value: null as any }),
|
isMatchingCurrencyFilter({
|
||||||
|
currencyFilter,
|
||||||
|
value: {
|
||||||
|
amountMicros: 10,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
},
|
||||||
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('value is NOT_NULL', () => {
|
it('amount micros filter does not match', () => {
|
||||||
const currencyFilter: CurrencyFilter = {
|
const currencyFilter: CurrencyFilter = {
|
||||||
amountMicros: { is: 'NOT_NULL' },
|
amountMicros: { eq: 10 },
|
||||||
|
currencyCode: { in: ['USD'] },
|
||||||
};
|
};
|
||||||
expect(isMatchingCurrencyFilter({ currencyFilter, value: 10 })).toBe(
|
expect(
|
||||||
true,
|
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 { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
||||||
import { z } from 'zod';
|
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 { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||||
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
|
import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator';
|
||||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
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';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
type ComputeFilterRecordGqlOperationFilterParams = {
|
type ComputeFilterRecordGqlOperationFilterParams = {
|
||||||
@ -61,14 +64,11 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
(field) => field.id === filter.fieldMetadataId,
|
(field) => field.id === filter.fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const compositeFieldName = filter.subFieldName;
|
const subFieldName = filter.subFieldName;
|
||||||
|
|
||||||
const isCompositeFieldFiter = isNonEmptyString(compositeFieldName);
|
const isSubFieldFilter = isNonEmptyString(subFieldName);
|
||||||
|
|
||||||
const isEmptinessOperand = [
|
const isAnEmptinessOperand = isEmptinessOperand(filter.operand);
|
||||||
RecordFilterOperand.IsEmpty,
|
|
||||||
RecordFilterOperand.IsNotEmpty,
|
|
||||||
].includes(filter.operand);
|
|
||||||
|
|
||||||
const isDateOperandWithoutValue = [
|
const isDateOperandWithoutValue = [
|
||||||
RecordFilterOperand.IsInPast,
|
RecordFilterOperand.IsInPast,
|
||||||
@ -85,7 +85,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
const isFilterValueEmpty = !isDefined(filter.value) || filter.value === '';
|
const isFilterValueEmpty = !isDefined(filter.value) || filter.value === '';
|
||||||
|
|
||||||
const shouldSkipFiltering =
|
const shouldSkipFiltering =
|
||||||
!isEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty;
|
!isAnEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty;
|
||||||
|
|
||||||
if (shouldSkipFiltering) {
|
if (shouldSkipFiltering) {
|
||||||
return;
|
return;
|
||||||
@ -98,7 +98,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
const filterHasEmptinessOperands =
|
const filterHasEmptinessOperands =
|
||||||
!filterTypesThatHaveNoEmptinessOperand.includes(filterType);
|
!filterTypesThatHaveNoEmptinessOperand.includes(filterType);
|
||||||
|
|
||||||
if (filterHasEmptinessOperands && isEmptinessOperand) {
|
if (filterHasEmptinessOperands && isAnEmptinessOperand) {
|
||||||
const emptyOperationFilter = getEmptyRecordGqlOperationFilter({
|
const emptyOperationFilter = getEmptyRecordGqlOperationFilter({
|
||||||
operand: filter.operand,
|
operand: filter.operand,
|
||||||
correspondingField,
|
correspondingField,
|
||||||
@ -357,25 +357,82 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'CURRENCY':
|
case 'CURRENCY': {
|
||||||
switch (filter.operand) {
|
if (
|
||||||
case RecordFilterOperand.GreaterThan:
|
isExpectedSubFieldName(
|
||||||
return {
|
FieldMetadataType.CURRENCY,
|
||||||
[correspondingField.name]: {
|
'currencyCode',
|
||||||
amountMicros: { gte: parseFloat(filter.value) * 1000000 },
|
subFieldName,
|
||||||
} as CurrencyFilter,
|
)
|
||||||
};
|
) {
|
||||||
case RecordFilterOperand.LessThan:
|
const parsedCurrencyCodes = JSON.parse(filter.value) as string[];
|
||||||
return {
|
|
||||||
[correspondingField.name]: {
|
if (parsedCurrencyCodes.length === 0) return undefined;
|
||||||
amountMicros: { lte: parseFloat(filter.value) * 1000000 },
|
|
||||||
} as CurrencyFilter,
|
const gqlFilter: RecordGqlOperationFilter = {
|
||||||
};
|
[correspondingField.name]: {
|
||||||
default:
|
currencyCode: { in: parsedCurrencyCodes },
|
||||||
throw new Error(
|
} as CurrencyFilter,
|
||||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
};
|
||||||
);
|
|
||||||
|
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': {
|
case 'LINKS': {
|
||||||
const linksFilters = generateILikeFiltersForCompositeFields(
|
const linksFilters = generateILikeFiltersForCompositeFields(
|
||||||
filter.value,
|
filter.value,
|
||||||
@ -385,21 +442,21 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
|
|
||||||
switch (filter.operand) {
|
switch (filter.operand) {
|
||||||
case RecordFilterOperand.Contains:
|
case RecordFilterOperand.Contains:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
or: linksFilters,
|
or: linksFilters,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case RecordFilterOperand.DoesNotContain:
|
case RecordFilterOperand.DoesNotContain:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
and: linksFilters.map((filter) => {
|
and: linksFilters.map((filter) => {
|
||||||
return {
|
return {
|
||||||
@ -411,7 +468,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
return {
|
return {
|
||||||
not: {
|
not: {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -432,21 +489,21 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
);
|
);
|
||||||
switch (filter.operand) {
|
switch (filter.operand) {
|
||||||
case RecordFilterOperand.Contains:
|
case RecordFilterOperand.Contains:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
or: fullNameFilters,
|
or: fullNameFilters,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case RecordFilterOperand.DoesNotContain:
|
case RecordFilterOperand.DoesNotContain:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
and: fullNameFilters.map((filter) => {
|
and: fullNameFilters.map((filter) => {
|
||||||
return {
|
return {
|
||||||
@ -458,7 +515,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
return {
|
return {
|
||||||
not: {
|
not: {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -474,7 +531,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
case 'ADDRESS':
|
case 'ADDRESS':
|
||||||
switch (filter.operand) {
|
switch (filter.operand) {
|
||||||
case RecordFilterOperand.Contains:
|
case RecordFilterOperand.Contains:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
or: [
|
or: [
|
||||||
{
|
{
|
||||||
@ -524,14 +581,14 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
} as AddressFilter,
|
} as AddressFilter,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case RecordFilterOperand.DoesNotContain:
|
case RecordFilterOperand.DoesNotContain:
|
||||||
if (!isCompositeFieldFiter) {
|
if (!isSubFieldFilter) {
|
||||||
return {
|
return {
|
||||||
and: [
|
and: [
|
||||||
{
|
{
|
||||||
@ -567,7 +624,7 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
return {
|
return {
|
||||||
not: {
|
not: {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
[compositeFieldName]: {
|
[subFieldName]: {
|
||||||
ilike: `%${filter.value}%`,
|
ilike: `%${filter.value}%`,
|
||||||
} as AddressFilter,
|
} 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 { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
|
||||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
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 { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
export type GetRecordFilterOperandsParams = {
|
export type GetRecordFilterOperandsParams = {
|
||||||
filterType: FilterableFieldType;
|
filterType: FilterableFieldType;
|
||||||
@ -21,6 +24,15 @@ type FilterOperandMap = {
|
|||||||
[K in FilterableFieldType]: readonly RecordFilterOperand[];
|
[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 = {
|
export const FILTER_OPERANDS_MAP = {
|
||||||
TEXT: [
|
TEXT: [
|
||||||
RecordFilterOperand.Contains,
|
RecordFilterOperand.Contains,
|
||||||
@ -113,6 +125,23 @@ export const FILTER_OPERANDS_MAP = {
|
|||||||
BOOLEAN: [RecordFilterOperand.Is],
|
BOOLEAN: [RecordFilterOperand.Is],
|
||||||
} as const satisfies FilterOperandMap;
|
} 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 = ({
|
export const getRecordFilterOperands = ({
|
||||||
filterType,
|
filterType,
|
||||||
subFieldName,
|
subFieldName,
|
||||||
@ -125,7 +154,29 @@ export const getRecordFilterOperands = ({
|
|||||||
case 'LINKS':
|
case 'LINKS':
|
||||||
case 'PHONES':
|
case 'PHONES':
|
||||||
return FILTER_OPERANDS_MAP.TEXT;
|
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':
|
case 'NUMBER':
|
||||||
return FILTER_OPERANDS_MAP.NUMBER;
|
return FILTER_OPERANDS_MAP.NUMBER;
|
||||||
case 'RAW_JSON':
|
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 { CurrencyFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const isMatchingCurrencyFilter = ({
|
const isMatchingCurrencyCodeFilter = (
|
||||||
currencyFilter,
|
currencyCodeFilter: CurrencyFilter['currencyCode'],
|
||||||
value,
|
value: string | null | undefined,
|
||||||
}: {
|
) => {
|
||||||
currencyFilter: CurrencyFilter;
|
|
||||||
value: number;
|
|
||||||
}) => {
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case currencyFilter.amountMicros?.eq !== undefined: {
|
case currencyCodeFilter?.in !== undefined: {
|
||||||
return value === currencyFilter.amountMicros.eq;
|
return isNonEmptyString(value) && currencyCodeFilter.in.includes(value);
|
||||||
}
|
}
|
||||||
case currencyFilter.amountMicros?.neq !== undefined: {
|
case currencyCodeFilter?.is !== undefined: {
|
||||||
return value !== currencyFilter.amountMicros.neq;
|
if (currencyCodeFilter.is === 'NULL') {
|
||||||
}
|
|
||||||
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') {
|
|
||||||
return value === null;
|
return value === null;
|
||||||
} else {
|
} else {
|
||||||
return value !== null;
|
return value !== null;
|
||||||
@ -38,10 +19,91 @@ export const isMatchingCurrencyFilter = ({
|
|||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected amountMicros for currency filter : ${JSON.stringify(
|
`Unexpected operand for currency code filter : ${JSON.stringify(
|
||||||
currencyFilter.amountMicros,
|
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;
|
const { value, operand } = recordFilter;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!isDefined(value) || value === '') &&
|
(!isDefined(value) || value === '' || value === '[]') &&
|
||||||
![
|
![
|
||||||
RecordFilterOperand.IsEmpty,
|
RecordFilterOperand.IsEmpty,
|
||||||
RecordFilterOperand.IsNotEmpty,
|
RecordFilterOperand.IsNotEmpty,
|
||||||
|
|||||||
@ -319,7 +319,7 @@ export const isRecordMatchingFilter = ({
|
|||||||
case FieldMetadataType.CURRENCY: {
|
case FieldMetadataType.CURRENCY: {
|
||||||
return isMatchingCurrencyFilter({
|
return isMatchingCurrencyFilter({
|
||||||
currencyFilter: filterValue as CurrencyFilter,
|
currencyFilter: filterValue as CurrencyFilter,
|
||||||
value: record[filterKey].amountMicros,
|
value: record[filterKey],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case FieldMetadataType.ACTOR: {
|
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 { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
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 { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
@ -103,8 +104,14 @@ export const useHandleToggleColumnFilter = ({
|
|||||||
|
|
||||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||||
|
|
||||||
|
const defaultSubFieldName =
|
||||||
|
getDefaultSubFieldNameForCompositeFilterableFieldType(
|
||||||
|
fieldMetadataItem.type,
|
||||||
|
);
|
||||||
|
|
||||||
const availableOperandsForFilter = getRecordFilterOperands({
|
const availableOperandsForFilter = getRecordFilterOperands({
|
||||||
filterType,
|
filterType,
|
||||||
|
subFieldName: defaultSubFieldName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultOperand = availableOperandsForFilter[0];
|
const defaultOperand = availableOperandsForFilter[0];
|
||||||
@ -117,6 +124,7 @@ export const useHandleToggleColumnFilter = ({
|
|||||||
label: fieldMetadataItem.label,
|
label: fieldMetadataItem.label,
|
||||||
type: filterType,
|
type: filterType,
|
||||||
value: '',
|
value: '',
|
||||||
|
subFieldName: defaultSubFieldName,
|
||||||
};
|
};
|
||||||
|
|
||||||
upsertRecordFilter(newFilter);
|
upsertRecordFilter(newFilter);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
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 {
|
import {
|
||||||
RecordFilter,
|
RecordFilter,
|
||||||
@ -25,7 +25,7 @@ export const buildValueFromFilter = ({
|
|||||||
currentWorkspaceMember?: CurrentWorkspaceMember;
|
currentWorkspaceMember?: CurrentWorkspaceMember;
|
||||||
label?: string;
|
label?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (isCompositeField(filter.type)) {
|
if (isCompositeFieldType(filter.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip';
|
import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip';
|
||||||
@ -57,19 +56,10 @@ export const MultipleSelectDropdown = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [itemsInDropdown, setItemInDropdown] = useState([
|
const itemsInDropdown = [
|
||||||
...(filteredSelectedItems ?? []),
|
...(filteredSelectedItems ?? []),
|
||||||
...(itemsToSelect ?? []),
|
...(itemsToSelect ?? []),
|
||||||
]);
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadingItems) {
|
|
||||||
setItemInDropdown([
|
|
||||||
...(filteredSelectedItems ?? []),
|
|
||||||
...(itemsToSelect ?? []),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}, [itemsToSelect, filteredSelectedItems, loadingItems]);
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape],
|
[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]: {
|
[FieldMetadataType.CURRENCY]: {
|
||||||
label: 'Currency',
|
label: 'Currency',
|
||||||
Icon: IllustrationIconCurrency,
|
Icon: IllustrationIconCurrency,
|
||||||
subFields: ['amountMicros'],
|
subFields: ['amountMicros', 'currencyCode'],
|
||||||
filterableSubFields: ['amountMicros'],
|
filterableSubFields: ['amountMicros', 'currencyCode'],
|
||||||
labelBySubField: {
|
labelBySubField: {
|
||||||
amountMicros: 'Amount',
|
amountMicros: 'Amount',
|
||||||
currencyCode: 'Currency',
|
currencyCode: 'Currency',
|
||||||
|
|||||||
@ -4,10 +4,9 @@ import { z } from 'zod';
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
|
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
|
||||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
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 { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { IconCurrencyDollar } from 'twenty-ui/display';
|
import { IconCurrencyDollar } from 'twenty-ui/display';
|
||||||
|
|
||||||
@ -24,14 +23,6 @@ type SettingsDataModelFieldCurrencyFormProps = {
|
|||||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue'>;
|
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPTIONS = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
|
||||||
([value, { label, Icon }]) => ({
|
|
||||||
label,
|
|
||||||
value: applySimpleQuotesToString(value),
|
|
||||||
Icon,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SettingsDataModelFieldCurrencyForm = ({
|
export const SettingsDataModelFieldCurrencyForm = ({
|
||||||
disabled,
|
disabled,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
@ -67,7 +58,7 @@ export const SettingsDataModelFieldCurrencyForm = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
dropdownId="object-field-default-value-select-currency"
|
dropdownId="object-field-default-value-select-currency"
|
||||||
options={OPTIONS}
|
options={CURRENCIES}
|
||||||
selectSizeVariant="small"
|
selectSizeVariant="small"
|
||||||
withSearchInput={true}
|
withSearchInput={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
@ -94,7 +94,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
|
|||||||
}
|
}
|
||||||
LeftIcon={getIcon(field.icon)}
|
LeftIcon={getIcon(field.icon)}
|
||||||
text={field.label}
|
text={field.label}
|
||||||
hasSubMenu={isCompositeField(field.type)}
|
hasSubMenu={isCompositeFieldType(field.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
@ -48,7 +48,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
onBack();
|
onBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isCompositeField(fieldMetadataItem.type)) {
|
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import { ReadonlyDeep } from 'type-fest';
|
import { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent';
|
import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent';
|
||||||
import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent';
|
import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent';
|
||||||
@ -40,7 +40,7 @@ export const MatchColumnToFieldSelect = ({
|
|||||||
) => {
|
) => {
|
||||||
setSelectedFieldMetadataItem(selectedFieldMetadataItem);
|
setSelectedFieldMetadataItem(selectedFieldMetadataItem);
|
||||||
|
|
||||||
if (!isCompositeField(selectedFieldMetadataItem.type)) {
|
if (!isCompositeFieldType(selectedFieldMetadataItem.type)) {
|
||||||
const correspondingOption = options.find(
|
const correspondingOption = options.find(
|
||||||
(option) => option.value === selectedFieldMetadataItem.name,
|
(option) => option.value === selectedFieldMetadataItem.name,
|
||||||
);
|
);
|
||||||
@ -100,11 +100,9 @@ export const MatchColumnToFieldSelect = ({
|
|||||||
(option) => option.value === DO_NOT_IMPORT_OPTION_KEY,
|
(option) => option.value === DO_NOT_IMPORT_OPTION_KEY,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDisplaySubFieldMetadataItemSelect = isDefined(
|
const shouldDisplaySubFieldMetadataItemSelect =
|
||||||
selectedFieldMetadataItem?.type,
|
isDefined(selectedFieldMetadataItem?.type) &&
|
||||||
)
|
isCompositeFieldType(selectedFieldMetadataItem?.type);
|
||||||
? isCompositeField(selectedFieldMetadataItem?.type)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
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 { 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 { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton';
|
||||||
|
import { Currency } from '@/ui/input/components/internal/types/Currency';
|
||||||
import { IMaskInput } from 'react-imask';
|
import { IMaskInput } from 'react-imask';
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
|
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
|
||||||
@ -50,12 +51,6 @@ export type CurrencyInputProps = {
|
|||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Currency = {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
Icon: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CurrencyInput = ({
|
export const CurrencyInput = ({
|
||||||
autoFocus,
|
autoFocus,
|
||||||
value,
|
value,
|
||||||
@ -96,19 +91,7 @@ export const CurrencyInput = ({
|
|||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencies = useMemo<Currency[]>(
|
const currency = CURRENCIES.find(({ value }) => value === currencyCode);
|
||||||
() =>
|
|
||||||
Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
|
||||||
([key, { Icon, label }]) => ({
|
|
||||||
value: key,
|
|
||||||
Icon,
|
|
||||||
label,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const currency = currencies.find(({ value }) => value === currencyCode);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalText(value);
|
setInternalText(value);
|
||||||
@ -119,9 +102,8 @@ export const CurrencyInput = ({
|
|||||||
return (
|
return (
|
||||||
<StyledContainer ref={wrapperRef}>
|
<StyledContainer ref={wrapperRef}>
|
||||||
<CurrencyPickerDropdownButton
|
<CurrencyPickerDropdownButton
|
||||||
valueCode={currency?.value ?? ''}
|
selectedCurrencyCode={currency?.value ?? ''}
|
||||||
onChange={handleCurrencyChange}
|
onChange={handleCurrencyChange}
|
||||||
currencies={currencies}
|
|
||||||
/>
|
/>
|
||||||
<StyledIcon>
|
<StyledIcon>
|
||||||
{Icon && (
|
{Icon && (
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
|
|
||||||
import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope';
|
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 { IconChevronDown } from 'twenty-ui/display';
|
||||||
|
import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect';
|
||||||
|
|
||||||
const StyledDropdownButtonContainer = styled.div`
|
const StyledDropdownButtonContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -41,20 +43,12 @@ const StyledIconContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type Currency = {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
Icon: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CurrencyPickerDropdownButton = ({
|
export const CurrencyPickerDropdownButton = ({
|
||||||
valueCode,
|
selectedCurrencyCode,
|
||||||
onChange,
|
onChange,
|
||||||
currencies,
|
|
||||||
}: {
|
}: {
|
||||||
valueCode: string;
|
selectedCurrencyCode: string;
|
||||||
onChange: (currency: Currency) => void;
|
onChange: (currency: Currency) => void;
|
||||||
currencies: Currency[];
|
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -67,7 +61,9 @@ export const CurrencyPickerDropdownButton = ({
|
|||||||
closeDropdown();
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const currency = currencies.find(({ value }) => value === valueCode);
|
const currency = CURRENCIES.find(
|
||||||
|
({ value }) => value === selectedCurrencyCode,
|
||||||
|
);
|
||||||
|
|
||||||
const currencyCode = currency?.value ?? CurrencyCode.USD;
|
const currencyCode = currency?.value ?? CurrencyCode.USD;
|
||||||
|
|
||||||
@ -85,7 +81,6 @@ export const CurrencyPickerDropdownButton = ({
|
|||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<CurrencyPickerDropdownSelect
|
<CurrencyPickerDropdownSelect
|
||||||
currencies={currencies}
|
|
||||||
selectedCurrency={currency}
|
selectedCurrency={currency}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,15 +4,15 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
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';
|
import { MenuItem, MenuItemSelectAvatar } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
export const CurrencyPickerDropdownSelect = ({
|
export const CurrencyPickerDropdownSelect = ({
|
||||||
currencies,
|
|
||||||
selectedCurrency,
|
selectedCurrency,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
currencies: Currency[];
|
|
||||||
selectedCurrency?: Currency;
|
selectedCurrency?: Currency;
|
||||||
onChange: (currency: Currency) => void;
|
onChange: (currency: Currency) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@ -20,14 +20,14 @@ export const CurrencyPickerDropdownSelect = ({
|
|||||||
|
|
||||||
const filteredCurrencies = useMemo(
|
const filteredCurrencies = useMemo(
|
||||||
() =>
|
() =>
|
||||||
currencies.filter(
|
CURRENCIES.filter(
|
||||||
({ value, label }) =>
|
({ value, label }) =>
|
||||||
value
|
value
|
||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(searchFilter.toLocaleLowerCase()) ||
|
.includes(searchFilter.toLocaleLowerCase()) ||
|
||||||
label.toLocaleLowerCase().includes(searchFilter.toLocaleLowerCase()),
|
label.toLocaleLowerCase().includes(searchFilter.toLocaleLowerCase()),
|
||||||
),
|
),
|
||||||
[currencies, searchFilter],
|
[searchFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -49,7 +49,7 @@ export const CurrencyPickerDropdownSelect = ({
|
|||||||
key={selectedCurrency.value}
|
key={selectedCurrency.value}
|
||||||
selected={true}
|
selected={true}
|
||||||
onClick={() => onChange(selectedCurrency)}
|
onClick={() => onChange(selectedCurrency)}
|
||||||
text={`${selectedCurrency.label} (${selectedCurrency.value})`}
|
text={selectedCurrency.label}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{filteredCurrencies.map((item) =>
|
{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 { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||||
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||||
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { isFilterOperandExpectingValue } from '@/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue';
|
|
||||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
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 { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
|
||||||
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
@ -27,11 +28,12 @@ export const EditableFilterChip = ({
|
|||||||
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
|
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
|
||||||
|
|
||||||
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
|
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
|
||||||
|
const operandIsEmptiness = isEmptinessOperand(recordFilter.operand);
|
||||||
|
|
||||||
const recordFilterSubFieldName = recordFilter.subFieldName;
|
const recordFilterSubFieldName = recordFilter.subFieldName;
|
||||||
|
|
||||||
const subFieldLabel =
|
const subFieldLabel =
|
||||||
isCompositeField(fieldMetadataItem.type) &&
|
isCompositeFieldType(fieldMetadataItem.type) &&
|
||||||
isNonEmptyString(recordFilterSubFieldName) &&
|
isNonEmptyString(recordFilterSubFieldName) &&
|
||||||
isValidSubFieldName(recordFilterSubFieldName)
|
isValidSubFieldName(recordFilterSubFieldName)
|
||||||
? getCompositeSubFieldLabel(
|
? getCompositeSubFieldLabel(
|
||||||
@ -44,11 +46,9 @@ export const EditableFilterChip = ({
|
|||||||
? `${recordFilter.label} / ${subFieldLabel}`
|
? `${recordFilter.label} / ${subFieldLabel}`
|
||||||
: recordFilter.label;
|
: recordFilter.label;
|
||||||
|
|
||||||
const shouldDisplayOperandLabelShort =
|
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
|
||||||
isNonEmptyString(recordFilter.value) ||
|
|
||||||
!isFilterOperandExpectingValue(recordFilter.operand);
|
|
||||||
|
|
||||||
const labelKey = `${fieldNameLabel}${shouldDisplayOperandLabelShort ? operandLabelShort : ''}`;
|
const labelKey = `${fieldNameLabel}${!operandIsEmptiness && !recordFilterIsEmpty ? operandLabelShort : operandIsEmptiness ? ` ${operandLabelShort}` : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortOrFilterChip
|
<SortOrFilterChip
|
||||||
|
|||||||
Reference in New Issue
Block a user