diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenu.tsx new file mode 100644 index 000000000..29a8a6545 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenu.tsx @@ -0,0 +1,81 @@ +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; + +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; + +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +import { AdvancedFilterDropdownFieldSelectMenuItem } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem'; +import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId'; +import { useFilterDropdownSelectableFieldMetadataItems } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems'; +import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useLingui } from '@lingui/react/macro'; + +export const AdvancedFilterDropdownFieldSelectMenu = () => { + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, + ); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const { + selectableHiddenFieldMetadataItems, + selectableVisibleFieldMetadataItems, + } = useFilterDropdownSelectableFieldMetadataItems(); + + const shouldShowSeparator = + selectableVisibleFieldMetadataItems.length > 0 && + selectableHiddenFieldMetadataItems.length > 0; + + const { t } = useLingui(); + + const selectableFieldMetadataItemIds = [ + ...selectableVisibleFieldMetadataItems.map( + (fieldMetadataItem) => fieldMetadataItem.id, + ), + ...selectableHiddenFieldMetadataItems.map( + (fieldMetadataItem) => fieldMetadataItem.id, + ), + ]; + + return ( + <> + ) => + setObjectFilterDropdownSearchInput(event.target.value) + } + /> + + + {selectableVisibleFieldMetadataItems.map( + (visibleFieldMetadataItem) => ( + + ), + )} + {shouldShowSeparator && } + {selectableHiddenFieldMetadataItems.map((hiddenFieldMetadataItem) => ( + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem.tsx similarity index 97% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx rename to packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem.tsx index ddb73212d..67a18e308 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem.tsx @@ -26,13 +26,13 @@ import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -export type ObjectFilterDropdownFilterSelectMenuItemProps = { +export type AdvancedFilterDropdownFieldSelectMenuItemProps = { fieldMetadataItemToSelect: FieldMetadataItem; }; -export const ObjectFilterDropdownFilterSelectMenuItem = ({ +export const AdvancedFilterDropdownFieldSelectMenuItem = ({ fieldMetadataItemToSelect, -}: ObjectFilterDropdownFilterSelectMenuItemProps) => { +}: AdvancedFilterDropdownFieldSelectMenuItemProps) => { const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( fieldMetadataItemIdUsedInDropdownComponentState, ); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx index 7afb23afb..1cf20a2c2 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx @@ -5,7 +5,9 @@ import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-d import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { AdvancedFilterDropdownTextInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput'; import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect'; +import { ObjectFilterDropdownCountrySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect'; import { ObjectFilterDropdownCurrencySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect'; import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput'; @@ -39,6 +41,12 @@ export const AdvancedFilterDropdownFilterInput = ({ return ( <> + {filterType === 'ADDRESS' && + (subFieldNameUsedInDropdown === 'addressCountry' ? ( + + ) : ( + + ))} {filterType === 'RATING' && } {DATE_FILTER_TYPES.includes(filterType) && ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownSubFieldSelectMenu.tsx similarity index 97% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx rename to packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownSubFieldSelectMenu.tsx index e6eff0d30..eb0f3adbc 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownSubFieldSelectMenu.tsx @@ -1,6 +1,5 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; -import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect'; import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; @@ -21,6 +20,7 @@ import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-re import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { isCompositeTypeFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -30,12 +30,13 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { StyledInput } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -export const ObjectFilterDropdownSubFieldSelect = () => { +export const AdvancedFilterDropdownSubFieldSelectMenu = () => { const [searchText, setSearchText] = useState(''); const { getIcon } = useIcons(); @@ -87,7 +88,7 @@ export const ObjectFilterDropdownSubFieldSelect = () => { const handleSelectFilter = ( fieldMetadataItem: FieldMetadataItem | null | undefined, - subFieldName?: string | null | undefined, + subFieldName?: CompositeFieldSubFieldName | null | undefined, ) => { if (!isDefined(fieldMetadataItem)) { return; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx index f26e9654f..fccd79aac 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelect.tsx @@ -1,10 +1,7 @@ -import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; -import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset'; +import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand'; -import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; -import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { SelectControl } from '@/ui/input/components/SelectControl'; @@ -41,42 +38,17 @@ export const AdvancedFilterRecordFilterOperandSelect = ({ (recordFilter) => recordFilter.id === recordFilterId, ); - const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); - const isDisabled = !filter?.fieldMetadataId; const { closeDropdown } = useDropdown(dropdownId); - const { upsertRecordFilter } = useUpsertRecordFilter(); + const { applyObjectFilterDropdownOperand } = + useApplyObjectFilterDropdownOperand(); const handleOperandChange = (operand: ViewFilterOperand) => { closeDropdown(); - if (!filter) { - throw new Error('Filter is not defined'); - } - - const fieldMetadataItem = getFieldMetadataItemById(filter.fieldMetadataId); - - if (!isDefined(fieldMetadataItem)) { - throw new Error('Field metadata item is not defined'); - } - - const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); - - const { value, displayValue } = getInitialFilterValue( - filterType, - operand, - filter.value, - filter.displayValue, - ); - - upsertRecordFilter({ - ...filter, - operand, - value, - displayValue, - }); + applyObjectFilterDropdownOperand(operand); }; const filterType = filter?.type; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx index f57b3e92c..60a484e16 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx @@ -15,6 +15,7 @@ import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/ 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 { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; @@ -57,7 +58,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({ const handleSelectFilter = ( selectedFieldMetadataItem: FieldMetadataItem | null | undefined, - subFieldName?: string | null | undefined, + subFieldName?: CompositeFieldSubFieldName | null | undefined, ) => { if (!isDefined(selectedFieldMetadataItem)) { return; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx index 9327afb69..3df320b64 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx @@ -7,6 +7,7 @@ import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/consta import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; 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'; @@ -36,6 +37,10 @@ export const AdvancedFilterValueInput = ({ currentRecordFiltersComponentState, ); + const subFieldNameUsedInDropdown = useRecoilComponentValueV2( + subFieldNameUsedInDropdownComponentState, + ); + const recordFilter = currentRecordFilters.find( (recordFilter) => recordFilter.id === recordFilterId, ); @@ -86,7 +91,9 @@ export const AdvancedFilterValueInput = ({ FieldMetadataType.CURRENCY, 'amountMicros', recordFilter.subFieldName, - ); + ) || + (filterType === 'ADDRESS' && + subFieldNameUsedInDropdown !== 'addressCountry'); return ( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts index a913d88b9..bc8126457 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts @@ -11,6 +11,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -20,7 +21,7 @@ import { isDefined } from 'twenty-shared/utils'; type SelectFilterParams = { fieldMetadataItemId: string; recordFilterId: string; - subFieldName?: string | null | undefined; + subFieldName?: CompositeFieldSubFieldName | null | undefined; }; export const useSelectFieldUsedInAdvancedFilterDropdown = () => { diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts index cd0ca0865..b41862826 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts @@ -2,8 +2,10 @@ import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/adv import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; +import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilCallback } from 'recoil'; @@ -28,26 +30,42 @@ export const useSetAdvancedFilterDropdownStates = () => { recordFilter.recordFilterGroupId === rootLevelRecordFilterGroup?.id, ); - for (const rootLevelRecordFilter of rootLevelRecordFilters) { + const setAdvancedFilterStatesForRecordFilter = ( + recordFilter: RecordFilter, + ) => { set( objectFilterDropdownCurrentRecordFilterComponentState.atomFamily({ instanceId: getAdvancedFilterObjectFilterDropdownComponentInstanceId( - rootLevelRecordFilter.id, + recordFilter.id, ), }), - rootLevelRecordFilter, + recordFilter, ); set( fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({ instanceId: getAdvancedFilterObjectFilterDropdownComponentInstanceId( - rootLevelRecordFilter.id, + recordFilter.id, ), }), - rootLevelRecordFilter.fieldMetadataId, + recordFilter.fieldMetadataId, ); + + set( + subFieldNameUsedInDropdownComponentState.atomFamily({ + instanceId: + getAdvancedFilterObjectFilterDropdownComponentInstanceId( + recordFilter.id, + ), + }), + recordFilter.subFieldName, + ); + }; + + for (const rootLevelRecordFilter of rootLevelRecordFilters) { + setAdvancedFilterStatesForRecordFilter(rootLevelRecordFilter); } const childRecordFilterGroups = currentRecordFilterGroups.filter( @@ -63,25 +81,7 @@ export const useSetAdvancedFilterDropdownStates = () => { ); for (const recordFilterInThisGroup of recordFiltersInThisGroup) { - set( - objectFilterDropdownCurrentRecordFilterComponentState.atomFamily({ - instanceId: - getAdvancedFilterObjectFilterDropdownComponentInstanceId( - recordFilterInThisGroup.id, - ), - }), - recordFilterInThisGroup, - ); - - set( - fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({ - instanceId: - getAdvancedFilterObjectFilterDropdownComponentInstanceId( - recordFilterInThisGroup.id, - ), - }), - recordFilterInThisGroup.fieldMetadataId, - ); + setAdvancedFilterStatesForRecordFilter(recordFilterInThisGroup); } } }, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx new file mode 100644 index 000000000..feb4693b8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx @@ -0,0 +1,146 @@ +import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; +import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; +import { getCountryFlagMenuItemAvatar } from '@/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar'; +import { turnCountryIntoSelectableItem } from '@/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'; +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 { useLingui } from '@lingui/react/macro'; +import { isNonEmptyString } from '@sniptt/guards'; +import { ChangeEvent, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation'; + +export const EMPTY_FILTER_VALUE = '[]'; +export const MAX_ITEMS_TO_DISPLAY = 5; + +type ObjectFilterDropdownCountrySelectProps = { + dropdownWidth?: number; +}; + +export const ObjectFilterDropdownCountrySelect = ({ + dropdownWidth, +}: ObjectFilterDropdownCountrySelectProps) => { + const [searchText, setSearchText] = useState(''); + + const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValueV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const { applyObjectFilterDropdownFilterValue } = + useApplyObjectFilterDropdownFilterValue(); + + const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const countries = useCountries(); + + const countriesAsSelectableItems = countries.map( + turnCountryIntoSelectableItem, + ); + + const selectedCountryNames = isNonEmptyString( + objectFilterDropdownCurrentRecordFilter?.value, + ) + ? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse + : []; + + const filteredSelectableItems = countriesAsSelectableItems.filter( + (selectableItem) => + selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) && + !selectedCountryNames.includes(selectableItem.name), + ); + + const filteredSelectedItems = countriesAsSelectableItems.filter( + (selectableItem) => + selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) && + selectedCountryNames.includes(selectableItem.name), + ); + + const handleMultipleItemSelectChange = ( + itemToSelect: SelectableItem, + newSelectedValue: boolean, + ) => { + const newSelectedItemNames = newSelectedValue + ? [...selectedCountryNames, itemToSelect.name] + : selectedCountryNames.filter((name) => name !== itemToSelect.name); + + if (!isDefined(fieldMetadataItemUsedInFilterDropdown)) { + throw new Error( + 'Field metadata item used in filter dropdown should be defined', + ); + } + + const selectedItemNames = countriesAsSelectableItems + .filter((option) => newSelectedItemNames.includes(option.name)) + .map((option) => option.name); + + const filterDisplayValue = + selectedItemNames.length > MAX_ITEMS_TO_DISPLAY + ? `${selectedItemNames.length} countries` + : selectedItemNames.join(', '); + + const newFilterValue = + newSelectedItemNames.length > 0 + ? JSON.stringify(selectedItemNames) + : EMPTY_FILTER_VALUE; + + applyObjectFilterDropdownFilterValue(newFilterValue, filterDisplayValue); + }; + + const showNoResult = + filteredSelectableItems.length === 0 && + filteredSelectedItems.length === 0 && + searchText !== ''; + + const { t } = useLingui(); + + return ( + <> + ) => { + setSearchText(event.target.value); + }} + /> + + + {filteredSelectedItems?.map((item) => { + return ( + { + handleMultipleItemSelectChange(item, newCheckedValue); + }} + text={item.name} + avatar={getCountryFlagMenuItemAvatar(item.name, countries)} + /> + ); + })} + {filteredSelectableItems?.map((item) => { + return ( + { + handleMultipleItemSelectChange(item, newCheckedValue); + }} + text={item.name} + avatar={getCountryFlagMenuItemAvatar(item.name, countries)} + /> + ); + })} + {showNoResult && } + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx index d10f7fcfa..dd79785a8 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx @@ -60,6 +60,8 @@ export const ObjectFilterDropdownCurrencySelect = ({ selectedCurrencies.includes(selectableItem.id), ); + const { t } = useLingui(); + const handleMultipleItemSelectChange = ( itemToSelect: SelectableItem, newSelectedValue: boolean, @@ -78,9 +80,11 @@ export const ObjectFilterDropdownCurrencySelect = ({ .filter((option) => newSelectedItemIds.includes(option.id)) .map((option) => option.name); + const currenciesLabel = t`currencies`; + const filterDisplayValue = selectedItemNames.length > MAX_ITEMS_TO_DISPLAY - ? `${selectedItemNames.length} currencies` + ? `${selectedItemNames.length} ${currenciesLabel}` : selectedItemNames.join(', '); const newFilterValue = @@ -96,8 +100,6 @@ export const ObjectFilterDropdownCurrencySelect = ({ filteredSelectedItems.length === 0 && searchText !== ''; - const { t } = useLingui(); - return ( <> {isConfigurable && selectedOperandInDropdown && ( @@ -99,7 +101,7 @@ export const ObjectFilterDropdownFilterInput = ({ )} {filterType === 'ACTOR' && - (isActorSourceCompositeFilter ? ( + (isActorSourceCompositeFilter || isNotASubFieldFilter ? ( <> @@ -108,6 +110,14 @@ export const ObjectFilterDropdownFilterInput = ({ ))} + {filterType === 'ADDRESS' && + (isNotASubFieldFilter ? ( + <> + + + ) : ( + <> + ))} {filterType === 'CURRENCY' && (isExpectedSubFieldName( FieldMetadataType.CURRENCY, @@ -126,7 +136,7 @@ export const ObjectFilterDropdownFilterInput = ({ ) : ( - <> + ))} {['SELECT', 'MULTI_SELECT'].includes(filterType) && ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/TextFilterTypes.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/TextFilterTypes.ts index ef63718ba..519326b99 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/TextFilterTypes.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/TextFilterTypes.ts @@ -5,7 +5,6 @@ export const TEXT_FILTER_TYPES = [ 'FULL_NAME', 'LINK', 'LINKS', - 'ADDRESS', 'ARRAY', 'RAW_JSON', ]; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts new file mode 100644 index 000000000..7b4f066d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts @@ -0,0 +1,56 @@ +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const useFilterDropdownSelectableFieldMetadataItems = () => { + const { recordIndexId } = useRecordIndexContextOrThrow(); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const { filterableFieldMetadataItems } = + useFilterableFieldMetadataItemsInRecordIndexContext(); + + const visibleTableColumns = useRecoilComponentValueV2( + visibleTableColumnsComponentSelector, + recordIndexId, + ); + + const visibleColumnsIds = visibleTableColumns.map( + (column) => column.fieldMetadataId, + ); + + const filteredSearchInputFieldMetadataItems = + filterableFieldMetadataItems.filter((fieldMetadataItem) => + fieldMetadataItem.label + .toLocaleLowerCase() + .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), + ); + + const selectableVisibleFieldMetadataItems = + filteredSearchInputFieldMetadataItems + .sort((a, b) => { + return ( + visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id) + ); + }) + .filter((fieldMetadataItem) => + visibleColumnsIds.includes(fieldMetadataItem.id), + ); + + const selectableHiddenFieldMetadataItems = + filteredSearchInputFieldMetadataItems + .sort((a, b) => a.label.localeCompare(b.label)) + .filter( + (fieldMetadataItem) => + !visibleColumnsIds.includes(fieldMetadataItem.id), + ); + + return { + selectableVisibleFieldMetadataItems, + selectableHiddenFieldMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterFromViewBarFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterFromViewBarFilterDropdown.ts new file mode 100644 index 000000000..d4c0a6e93 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterFromViewBarFilterDropdown.ts @@ -0,0 +1,84 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; +import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; +import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { isDefined } from 'twenty-shared/utils'; + +export const useSelectFilterFromViewBarFilterDropdown = () => { + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + + const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); + + const setHotkeyScope = useSetHotkeyScope(); + + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + const setObjectFilterDropdownCurrentRecordFilter = + useSetRecoilComponentStateV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const selectFilterFromViewBarFilterDropdown = ( + fieldMetadataItem: FieldMetadataItem, + ) => { + setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id); + + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + + if (filterType === 'RELATION' || filterType === 'SELECT') { + setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker); + } + + const defaultOperand = getRecordFilterOperands({ + filterType, + })[0]; + + setObjectFilterDropdownFilterIsSelected(true); + + const duplicateFilterInCurrentRecordFilters = + findDuplicateRecordFilterInNonAdvancedRecordFilters({ + recordFilters: currentRecordFilters, + fieldMetadataItemId: fieldMetadataItem.id, + }); + + const filterIsAlreadyInCurrentRecordFilters = isDefined( + duplicateFilterInCurrentRecordFilters, + ); + + if (filterIsAlreadyInCurrentRecordFilters) { + setObjectFilterDropdownCurrentRecordFilter( + duplicateFilterInCurrentRecordFilters, + ); + + setSelectedOperandInDropdown( + duplicateFilterInCurrentRecordFilters.operand, + ); + } else { + setSelectedOperandInDropdown(defaultOperand); + } + }; + + return { + selectFilterFromViewBarFilterDropdown, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts index 4a5429b05..66058e692 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts @@ -1,8 +1,9 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const subFieldNameUsedInDropdownComponentState = createComponentStateV2< - string | null | undefined + CompositeFieldSubFieldName | null | undefined >({ key: 'subFieldNameUsedInDropdownComponentState', defaultValue: null, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar.tsx new file mode 100644 index 000000000..f406a6ff8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar.tsx @@ -0,0 +1,16 @@ +import { Country } from '@/ui/input/components/internal/types/Country'; + +export const getCountryFlagMenuItemAvatar = ( + countryName: string, + countries: Country[], +): React.ReactNode => { + const country = countries.find( + (country) => country.countryName === countryName, + ); + + if (!country) { + return
; + } + + return country.Flag({ width: 20 }); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem.ts new file mode 100644 index 000000000..27b0ead72 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem.ts @@ -0,0 +1,10 @@ +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { Country } from '@/ui/input/components/internal/types/Country'; + +export const turnCountryIntoSelectableItem = ( + country: Country, +): SelectableItem => ({ + id: country.countryCode, + name: `${country.countryName}`, + isSelected: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index 63c77a3a7..8997740ef 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -3,7 +3,6 @@ import { useEffect } from 'react'; import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; @@ -13,6 +12,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; 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 { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -118,7 +118,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { > {t`Group by`} - = diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index b2ce256be..32056092e 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -220,31 +220,124 @@ describe('should work as expected for the different field types', () => { { and: [ { - not: { - address: { - addressStreet1: { - ilike: '%123 Main St%', + or: [ + { + not: { + address: { + addressStreet1: { + ilike: '%123 Main St%', + }, + }, }, }, - }, + { + address: { + addressStreet1: { + is: 'NULL', + }, + }, + }, + ], }, { - not: { - address: { - addressStreet2: { - ilike: '%123 Main St%', + or: [ + { + not: { + address: { + addressStreet2: { + ilike: '%123 Main St%', + }, + }, }, }, - }, + { + address: { + addressStreet2: { + is: 'NULL', + }, + }, + }, + ], }, { - not: { - address: { - addressCity: { - ilike: '%123 Main St%', + or: [ + { + not: { + address: { + addressCity: { + ilike: '%123 Main St%', + }, + }, }, }, - }, + { + address: { + addressCity: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + address: { + addressState: { + ilike: '%123 Main St%', + }, + }, + }, + }, + { + address: { + addressState: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + address: { + addressPostcode: { + ilike: '%123 Main St%', + }, + }, + }, + }, + { + address: { + addressPostcode: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + address: { + addressCountry: { + ilike: '%123 Main St%', + }, + }, + }, + }, + { + address: { + addressCountry: { + is: 'NULL', + }, + }, + }, + ], }, ], }, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts index f46af31db..79af110c0 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts @@ -4,6 +4,7 @@ const COMPOSITE_TYPES_FILTERABLE = [ 'ACTOR', 'FULL_NAME', 'CURRENCY', + 'ADDRESS', ] satisfies FieldType[]; type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number]; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts index 977984f34..ac34a8061 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts @@ -392,7 +392,8 @@ export const computeFilterRecordGqlOperationFilter = ({ FieldMetadataType.CURRENCY, 'amountMicros', subFieldName, - ) + ) || + !isSubFieldFilter ) { switch (filter.operand) { case RecordFilterOperand.GreaterThan: @@ -579,6 +580,22 @@ export const computeFilterRecordGqlOperationFilter = ({ ], }; } else { + if (subFieldName === 'addressCountry') { + const parsedCountryCodes = JSON.parse(filter.value) as string[]; + + if (filter.value === '[]' || parsedCountryCodes.length === 0) { + return {}; + } + + return { + [correspondingField.name]: { + [subFieldName]: { + in: parsedCountryCodes, + } as AddressFilter, + }, + }; + } + return { [correspondingField.name]: { [subFieldName]: { @@ -592,43 +609,176 @@ export const computeFilterRecordGqlOperationFilter = ({ return { and: [ { - not: { - [correspondingField.name]: { - addressStreet1: { - ilike: `%${filter.value}%`, + or: [ + { + not: { + [correspondingField.name]: { + addressStreet1: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, }, - } as AddressFilter, - }, + }, + { + [correspondingField.name]: { + addressStreet1: { + is: 'NULL', + }, + }, + }, + ], }, { - not: { - [correspondingField.name]: { - addressStreet2: { - ilike: `%${filter.value}%`, + or: [ + { + not: { + [correspondingField.name]: { + addressStreet2: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, }, - } as AddressFilter, - }, + }, + { + [correspondingField.name]: { + addressStreet2: { + is: 'NULL', + }, + }, + }, + ], }, { - not: { - [correspondingField.name]: { - addressCity: { - ilike: `%${filter.value}%`, + or: [ + { + not: { + [correspondingField.name]: { + addressCity: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, }, - } as AddressFilter, - }, + }, + { + [correspondingField.name]: { + addressCity: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + [correspondingField.name]: { + addressState: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + { + [correspondingField.name]: { + addressState: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + [correspondingField.name]: { + addressPostcode: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + { + [correspondingField.name]: { + addressPostcode: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + not: { + [correspondingField.name]: { + addressCountry: { + ilike: `%${filter.value}%`, + }, + } as AddressFilter, + }, + }, + { + [correspondingField.name]: { + addressCountry: { + is: 'NULL', + }, + }, + }, + ], }, ], }; } else { + if (subFieldName === 'addressCountry') { + const parsedCountryCodes = JSON.parse(filter.value) as string[]; + + if (filter.value === '[]' || parsedCountryCodes.length === 0) { + return {}; + } + + return { + or: [ + { + not: { + [correspondingField.name]: { + addressCountry: { + in: JSON.parse(filter.value), + } as AddressFilter, + }, + }, + }, + { + [correspondingField.name]: { + addressCountry: { + is: 'NULL', + } as AddressFilter, + }, + }, + ], + }; + } + return { - not: { - [correspondingField.name]: { - [subFieldName]: { - ilike: `%${filter.value}%`, - } as AddressFilter, + or: [ + { + not: { + [correspondingField.name]: { + [subFieldName]: { + ilike: `%${filter.value}%`, + } as AddressFilter, + }, + }, }, - }, + { + [correspondingField.name]: { + [subFieldName]: { + is: 'NULL', + } as AddressFilter, + }, + }, + ], }; } default: diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts index b0376be7f..07e9ea5bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType.ts @@ -17,17 +17,17 @@ export const getDefaultSubFieldNameForCompositeFilterableFieldType = ( case 'CURRENCY': return 'amountMicros'; case 'LINKS': - return 'primaryLinkUrl'; + return undefined; case 'PHONES': - return 'primaryPhoneNumber'; + return undefined; case 'EMAILS': - return 'primaryEmail'; + return undefined; case 'ADDRESS': - return 'addressCity'; + return undefined; case 'ACTOR': return 'source'; case 'FULL_NAME': - return 'firstName'; + return undefined; default: assertUnreachable(compositeFieldType); } diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts index 236ab6f35..828e68462 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts @@ -164,18 +164,8 @@ export const getRecordFilterOperands = ({ ) ) { 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`, - ); + return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.amountMicros; } } case 'NUMBER': diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 3d7fdb9ec..61970bafd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -163,7 +163,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { ); useScopedHotkeys( - [Key.ArrowUp, 'k'], + [Key.ArrowUp], () => { setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); move('up'); @@ -173,7 +173,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { ); useScopedHotkeys( - [Key.ArrowDown, 'j'], + [Key.ArrowDown], () => { setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); move('down'); diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx index dd2734c38..724fd9d14 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx @@ -85,7 +85,7 @@ export const MultipleSelectDropdown = ({ selectableItemIdArray={selectableItemIds} hotkeyScope={hotkeyScope} > - + {itemsInDropdown?.map((item) => { return ( = T & { id: string; name: string; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx index dff3441e3..930fe37dc 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx @@ -76,7 +76,7 @@ export const AddressInput = ({ const addressStreet2InputRef = useRef(null); const addressCityInputRef = useRef(null); const addressStateInputRef = useRef(null); - const addressPostCodeInputRef = useRef(null); + const addressPostcodeInputRef = useRef(null); const inputRefs: { [key in keyof FieldAddressDraftValue]?: RefObject; @@ -85,7 +85,7 @@ export const AddressInput = ({ addressStreet2: addressStreet2InputRef, addressCity: addressCityInputRef, addressState: addressStateInputRef, - addressPostcode: addressPostCodeInputRef, + addressPostcode: addressPostcodeInputRef, }; const [focusPosition, setFocusPosition] = diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx index 450417761..62678bde2 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx @@ -51,6 +51,7 @@ export const ViewBarFilterDropdown = ({ dropdownHotkeyScope={hotkeyScope} dropdownOffset={{ y: 8 }} onClickOutside={handleDropdownClickOutside} + dropdownWidth={208} /> ); }; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx index e1e197a12..240e2e832 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx @@ -1,27 +1,16 @@ -import { ObjectFilterDropdownSubFieldSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect'; import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; -import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton'; +import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu'; import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId'; -import { ObjectFilterDropdownFieldSelect } from '../../object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect'; export const ViewBarFilterDropdownContent = () => { - const [objectFilterDropdownIsSelectingCompositeField] = - useRecoilComponentStateV2( - objectFilterDropdownIsSelectingCompositeFieldComponentState, - VIEW_BAR_FILTER_DROPDOWN_ID, - ); - const [objectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2( objectFilterDropdownFilterIsSelectedComponentState, VIEW_BAR_FILTER_DROPDOWN_ID, ); - const shouldShowCompositeSelectionSubMenu = - objectFilterDropdownIsSelectingCompositeField; - const shouldShowFilterInput = objectFilterDropdownFilterIsSelected; return ( @@ -30,11 +19,9 @@ export const ViewBarFilterDropdownContent = () => { - ) : shouldShowCompositeSelectionSubMenu ? ( - ) : ( <> - + )} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx similarity index 53% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx rename to packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx index 9c1b009f7..f969c0e31 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx @@ -2,20 +2,16 @@ import styled from '@emotion/styled'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; - import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; -import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId'; +import { useFilterDropdownSelectableFieldMetadataItems } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; -import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { ViewBarFilterDropdownFieldSelectMenuItem } from '@/views/components/ViewBarFilterDropdownFieldSelectMenuItem'; import { useLingui } from '@lingui/react/macro'; export const StyledInput = styled.input` @@ -44,59 +40,30 @@ export const StyledInput = styled.input` } `; -export const ObjectFilterDropdownFieldSelect = () => { - const { recordIndexId } = useRecordIndexContextOrThrow(); - +export const ViewBarFilterDropdownFieldSelectMenu = () => { const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] = useRecoilComponentStateV2(objectFilterDropdownSearchInputComponentState); - const { filterableFieldMetadataItems } = - useFilterableFieldMetadataItemsInRecordIndexContext(); - - const visibleTableColumns = useRecoilComponentValueV2( - visibleTableColumnsComponentSelector, - recordIndexId, - ); - const visibleColumnsIds = visibleTableColumns.map( - (column) => column.fieldMetadataId, - ); - - const filteredSearchInputFieldMetadataItems = - filterableFieldMetadataItems.filter((fieldMetadataItem) => - fieldMetadataItem.label - .toLocaleLowerCase() - .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), - ); - - const visibleColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems - .sort((a, b) => { - return visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id); - }) - .filter((fieldMetadataItem) => - visibleColumnsIds.includes(fieldMetadataItem.id), - ); - - const hiddenColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems - .sort((a, b) => a.label.localeCompare(b.label)) - .filter( - (fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id), - ); - - const shouldShowSeparator = - visibleColumnsFieldMetadataItems.length > 0 && - hiddenColumnsFieldMetadataItems.length > 0; - - const { t } = useLingui(); + const { + selectableHiddenFieldMetadataItems, + selectableVisibleFieldMetadataItems, + } = useFilterDropdownSelectableFieldMetadataItems(); const selectableFieldMetadataItemIds = [ - ...visibleColumnsFieldMetadataItems.map( + ...selectableVisibleFieldMetadataItems.map( (fieldMetadataItem) => fieldMetadataItem.id, ), - ...hiddenColumnsFieldMetadataItems.map( + ...selectableHiddenFieldMetadataItems.map( (fieldMetadataItem) => fieldMetadataItem.id, ), ]; + const shouldShowSeparator = + selectableVisibleFieldMetadataItems.length > 0 && + selectableHiddenFieldMetadataItems.length > 0; + + const { t } = useLingui(); + return ( <> { selectableListInstanceId={FILTER_FIELD_LIST_ID} > - {visibleColumnsFieldMetadataItems.map((visibleFieldMetadataItem) => ( - - ))} + {selectableVisibleFieldMetadataItems.map( + (visibleFieldMetadataItem) => ( + + ), + )} {shouldShowSeparator && } - {hiddenColumnsFieldMetadataItems.map((hiddenFieldMetadataItem) => ( - ( + diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx new file mode 100644 index 000000000..3d6f1c513 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx @@ -0,0 +1,124 @@ +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; +import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; + +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId'; +import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; + +export type ViewBarFilterDropdownFieldSelectMenuItemProps = { + fieldMetadataItemToSelect: FieldMetadataItem; +}; + +export const ViewBarFilterDropdownFieldSelectMenuItem = ({ + fieldMetadataItemToSelect, +}: ViewBarFilterDropdownFieldSelectMenuItemProps) => { + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + + const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + + const { resetSelectedItem } = useSelectableList(FILTER_FIELD_LIST_ID); + + const isSelectedItem = useRecoilComponentFamilyValueV2( + isSelectedItemIdComponentFamilySelector, + fieldMetadataItemToSelect.id, + ); + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); + + const setHotkeyScope = useSetHotkeyScope(); + + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + const setObjectFilterDropdownCurrentRecordFilter = + useSetRecoilComponentStateV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const handleSelectFilter = (fieldMetadataItem: FieldMetadataItem) => { + setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id); + + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + + if (filterType === 'RELATION' || filterType === 'SELECT') { + setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker); + } + + const defaultOperand = getRecordFilterOperands({ + filterType, + })[0]; + + setObjectFilterDropdownFilterIsSelected(true); + + const duplicateFilterInCurrentRecordFilters = + findDuplicateRecordFilterInNonAdvancedRecordFilters({ + recordFilters: currentRecordFilters, + fieldMetadataItemId: fieldMetadataItem.id, + }); + + const filterIsAlreadyInCurrentRecordFilters = isDefined( + duplicateFilterInCurrentRecordFilters, + ); + + if (filterIsAlreadyInCurrentRecordFilters) { + setObjectFilterDropdownCurrentRecordFilter( + duplicateFilterInCurrentRecordFilters, + ); + + setSelectedOperandInDropdown( + duplicateFilterInCurrentRecordFilters.operand, + ); + } else { + setSelectedOperandInDropdown(defaultOperand); + } + }; + + const { getIcon } = useIcons(); + + const Icon = getIcon(fieldMetadataItemToSelect.icon); + + const handleClick = () => { + resetSelectedItem(); + + handleSelectFilter(fieldMetadataItemToSelect); + }; + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilter.ts b/packages/twenty-front/src/modules/views/types/ViewFilter.ts index b5e5160ba..59f56e9d3 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilter.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilter.ts @@ -1,3 +1,4 @@ +import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { ViewFilterOperand } from './ViewFilterOperand'; export type ViewFilter = { @@ -13,5 +14,5 @@ export type ViewFilter = { viewId?: string; viewFilterGroupId?: string; positionInViewFilterGroup?: number | null; - subFieldName?: string | null; + subFieldName?: CompositeFieldSubFieldName | null; }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index 35e224bd4..b514febfb 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -115,6 +115,16 @@ export class GraphqlQueryFilterFieldParser { subFieldFilter as Record, ); + if ( + ARRAY_OPERATORS.includes(operator) && + (!Array.isArray(value) || value.length === 0) + ) { + throw new GraphqlQueryRunnerException( + `Invalid filter value for field ${subFieldKey}. Expected non-empty array`, + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + const { sql, params } = computeWhereConditionParts( operator, objectNameSingular, diff --git a/packages/twenty-server/src/engine/seeder/data-seeds/pets-data-seeds.ts b/packages/twenty-server/src/engine/seeder/data-seeds/pets-data-seeds.ts index 6596643c6..a17f5c6a4 100644 --- a/packages/twenty-server/src/engine/seeder/data-seeds/pets-data-seeds.ts +++ b/packages/twenty-server/src/engine/seeder/data-seeds/pets-data-seeds.ts @@ -11,7 +11,7 @@ export const PETS_DATA_SEEDS = [ addressStreet2: '7344 Haley Loop', addressCity: 'Jacksonstad', addressCountry: 'United States', - addressPostCode: '32048-5208', + addressPostcode: '32048-5208', addressState: 'North Dakota', }, vetPhone: { diff --git a/packages/twenty-ui/src/display/icon/types/IconComponent.ts b/packages/twenty-ui/src/display/icon/types/IconComponent.ts index 014898aa0..1e8ce1218 100644 --- a/packages/twenty-ui/src/display/icon/types/IconComponent.ts +++ b/packages/twenty-ui/src/display/icon/types/IconComponent.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line no-restricted-imports import { FunctionComponent } from 'react'; export type IconComponentProps = {