From afea017c127b5055ead5170873636c0e81953898 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 9 May 2025 11:32:46 +0200 Subject: [PATCH] Sub-field filtering on ADDRESS type (#11912) This PR adds what's needed to filter on the ADDRESS sub-fields, notably the country sub-field, that requires a country multi select component, which was created in this PR (ObjectFilterDropdownCountrySelect) This PR refactors the common logic between advanced filter dropdown field selection logic and view bar filter dropdown field selection logic, notably in useFilterDropdownSelectableFieldMetadataItems. There are now new components to identify clearly what's tied to view bar or advanced filter, it could be further simplified or factorized, but as it is right now, it's simple enough to be maintained easily even if a little bit too verbose, which is often the best trade-off we should aim for. Improvements : - Added the CompositeFieldSubFieldName where needed - Fixes bug in advanced filter dropdown input - Fixes dropdown content width bug in advanced filter dropdown input - Fixes a bug when inputing a Currency filter without a sub-field in view bar filter dropdown - Used DropdownMenuSearchInput instead of a custom StyledInput which was doing exactly the same thing - Factorized the state setting logic in useSetAdvancedFilterDropdownStates in an anonymous function setAdvancedFilterDropdownStates - Created useSelectFilterFromViewBarFilterDropdown hook to have a more meaningful and clear logic to abstract what happens when we select a field to filter in the view bard field select dropdown - Fixes a bug with advanced filter operand dropdown select which wasn't modifying the current record filter and creating a stale state. Fixes https://github.com/twentyhq/core-team-issues/issues/612 --- .../AdvancedFilterDropdownFieldSelectMenu.tsx | 81 +++++++ ...ncedFilterDropdownFieldSelectMenuItem.tsx} | 6 +- .../AdvancedFilterDropdownFilterInput.tsx | 8 + ...ancedFilterDropdownSubFieldSelectMenu.tsx} | 7 +- ...dvancedFilterRecordFilterOperandSelect.tsx | 36 +--- .../AdvancedFilterSubFieldSelectMenu.tsx | 3 +- .../components/AdvancedFilterValueInput.tsx | 9 +- ...SelectFieldUsedInAdvancedFilterDropdown.ts | 3 +- ...eSetAdvancedFilterDropdownAllRowsStates.ts | 48 ++--- .../ObjectFilterDropdownCountrySelect.tsx | 146 +++++++++++++ .../ObjectFilterDropdownCurrencySelect.tsx | 8 +- .../ObjectFilterDropdownFilterInput.tsx | 14 +- .../constants/TextFilterTypes.ts | 1 - ...terDropdownSelectableFieldMetadataItems.ts | 56 +++++ ...seSelectFilterFromViewBarFilterDropdown.ts | 84 ++++++++ ...ubFieldNameUsedInDropdownComponentState.ts | 3 +- .../utils/getCountryFlagMenuItemAvatar.tsx | 16 ++ .../utils/turnCountryIntoSelectableItem.ts | 10 + ...ptionsDropdownRecordGroupFieldsContent.tsx | 4 +- .../record-filter/types/RecordFilter.ts | 3 +- ...omputeViewRecordGqlOperationFilter.test.ts | 123 +++++++++-- .../areCompositeTypeSubFieldsFilterable.ts | 1 + .../computeViewRecordGqlOperationFilter.ts | 200 +++++++++++++++--- ...ieldNameForCompositeFilterableFieldType.ts | 10 +- .../utils/getRecordFilterOperands.ts | 12 +- .../record-table/hooks/useRecordTable.ts | 4 +- .../components/MultipleSelectDropdown.tsx | 2 +- .../select/types/SelectableItem.ts | 1 + .../field/input/components/AddressInput.tsx | 4 +- .../components/ViewBarFilterDropdown.tsx | 1 + .../ViewBarFilterDropdownContent.tsx | 17 +- .../ViewBarFilterDropdownFieldSelectMenu.tsx} | 81 +++---- ...ewBarFilterDropdownFieldSelectMenuItem.tsx | 124 +++++++++++ .../src/modules/views/types/ViewFilter.ts | 3 +- .../graphql-query-filter-field.parser.ts | 10 + .../seeder/data-seeds/pets-data-seeds.ts | 2 +- .../src/display/icon/types/IconComponent.ts | 1 - 37 files changed, 932 insertions(+), 210 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenu.tsx rename packages/twenty-front/src/modules/object-record/{object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx => advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem.tsx} (97%) rename packages/twenty-front/src/modules/object-record/{object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect.tsx => advanced-filter/components/AdvancedFilterDropdownSubFieldSelectMenu.tsx} (97%) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterFromViewBarFilterDropdown.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem.ts rename packages/twenty-front/src/modules/{object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect.tsx => views/components/ViewBarFilterDropdownFieldSelectMenu.tsx} (53%) create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx 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 = {