From 6bb7042a688f064841cae8cc9500f4399d3dfadb Mon Sep 17 00:00:00 2001 From: Arshil Vahora <70322256+ARSHIL1804@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:11:41 +0530 Subject: [PATCH] Select Field Input Menu scrollable and add Select Field in Filter and Sort (#3656) * - fix Select Option Menu scrollable and added search - add select field in filter and sort operation * Fix lint * Fix post merge * Fix select filter * Fix * Remove duplicated search input * fix turn object into query * Rename search inputs * Remove debounced for options * Simplify option filter * Rename option to MenuItemSelectTag * Fix test * Infer type from field metadata item --------- Co-authored-by: Charles Bochet Co-authored-by: Thomas Trompette --- .../types/FieldMetadataItem.ts | 16 +-- ...atFieldMetadataItemsAsFilterDefinitions.ts | 5 +- ...rmatFieldMetadataItemsAsSortDefinitions.ts | 1 + .../MultipleFiltersDropdownContent.tsx | 20 +++- ....tsx => ObjectFilterDropdownDateInput.tsx} | 2 +- ...sx => ObjectFilterDropdownNumberInput.tsx} | 6 +- .../ObjectFilterDropdownOptionSelect.tsx | 111 ++++++++++++++++++ ...sx => ObjectFilterDropdownSearchInput.tsx} | 2 +- ...SingleEntityObjectFilterDropdownButton.tsx | 4 +- .../hooks/useFilterDropdown.ts | 4 + .../hooks/useFilterDropdownStates.ts | 11 ++ .../hooks/useOptionsForSelect.ts | 26 ++++ ...DropdownSelectedOptionValuesScopedState.ts | 7 ++ .../types/FilterType.ts | 3 +- .../utils/getOperandsForFilterType.ts | 1 + .../input/components/SelectFieldInput.tsx | 33 +++++- .../utils/isRecordMatchingFilter.ts | 1 + ...turnObjectDropdownFilterIntoQueryFilter.ts | 44 +++++++ .../components/MenuItemSelectTag.tsx | 40 +++++++ .../views/components/ViewBarFilterEffect.tsx | 18 +++ .../__tests__/args-string.factory.spec.ts | 36 ++++++ .../factories/args-string.factory.ts | 3 + 22 files changed, 367 insertions(+), 27 deletions(-) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/{ObjectFilterDropdownDateSearchInput.tsx => ObjectFilterDropdownDateInput.tsx} (94%) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/{ObjectFilterDropdownNumberSearchInput.tsx => ObjectFilterDropdownNumberInput.tsx} (81%) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/{ObjectFilterDropdownEntitySearchInput.tsx => ObjectFilterDropdownSearchInput.tsx} (93%) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts create mode 100644 packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectTag.tsx diff --git a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts index 9cf5f15d3..feaa73ed9 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts @@ -1,6 +1,14 @@ import { ThemeColor } from '@/ui/theme/constants/MainColorNames'; import { Field, Relation } from '~/generated-metadata/graphql'; +export type FieldMetadataItemOption = { + color: ThemeColor; + id: string; + label: string; + position: number; + value: string; +}; + export type FieldMetadataItem = Omit< Field, | '__typename' @@ -27,11 +35,5 @@ export type FieldMetadataItem = Omit< }) | null; defaultValue?: any; - options?: { - color: ThemeColor; - id: string; - label: string; - position: number; - value: string; - }[]; + options?: FieldMetadataItemOption[]; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index 59e622f86..7dcf29ab0 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -18,6 +18,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ FieldMetadataType.Link, FieldMetadataType.FullName, FieldMetadataType.Relation, + FieldMetadataType.Select, FieldMetadataType.Currency, ].includes(field.type) ) { @@ -67,5 +68,7 @@ export const formatFieldMetadataItemAsFilterDefinition = ({ ? 'TEXT' : field.type === FieldMetadataType.Relation ? 'RELATION' - : 'TEXT', + : field.type === FieldMetadataType.Select + ? 'SELECT' + : 'TEXT', }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts index 736b8b512..4e48dcfa9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts @@ -15,6 +15,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({ FieldMetadataType.Number, FieldMetadataType.Text, FieldMetadataType.Boolean, + FieldMetadataType.Select, ].includes(field.type) ) { return acc; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 67dc3c3d4..070d8bf8e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,13 +1,14 @@ -import { ObjectFilterDropdownRecordSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput'; +import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; -import { ObjectFilterDropdownDateSearchInput } from './ObjectFilterDropdownDateSearchInput'; +import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; -import { ObjectFilterDropdownNumberSearchInput } from './ObjectFilterDropdownNumberSearchInput'; +import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInput'; import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton'; import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect'; +import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect'; import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; @@ -40,17 +41,24 @@ export const MultipleFiltersDropdownContent = ({ ) && } {['NUMBER', 'CURRENCY'].includes( filterDefinitionUsedInDropdown.type, - ) && } + ) && } {filterDefinitionUsedInDropdown.type === 'DATE_TIME' && ( - + )} {filterDefinitionUsedInDropdown.type === 'RELATION' && ( <> - + )} + {filterDefinitionUsedInDropdown.type === 'SELECT' && ( + <> + + + + + )} ) )} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx similarity index 94% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateSearchInput.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index f397312f4..19b5a9897 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -2,7 +2,7 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/ import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { isNonNullable } from '~/utils/isNonNullable'; -export const ObjectFilterDropdownDateSearchInput = () => { +export const ObjectFilterDropdownDateInput = () => { const { filterDefinitionUsedInDropdown, selectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx similarity index 81% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberSearchInput.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx index e99411065..aec637714 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx @@ -1,9 +1,9 @@ import { ChangeEvent } from 'react'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; -export const ObjectFilterDropdownNumberSearchInput = () => { +export const ObjectFilterDropdownNumberInput = () => { const { selectedOperandInDropdown, filterDefinitionUsedInDropdown, @@ -13,7 +13,7 @@ export const ObjectFilterDropdownNumberSearchInput = () => { return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( - { + const { + filterDefinitionUsedInDropdown, + objectFilterDropdownSearchInput, + selectedOperandInDropdown, + objectFilterDropdownSelectedOptionValues, + selectFilter, + } = useFilterDropdown(); + + const fieldMetaDataId = filterDefinitionUsedInDropdown?.fieldMetadataId ?? ''; + + const { selectOptions } = useOptionsForSelect(fieldMetaDataId); + + const [selectableOptions, setSelectableOptions] = useState< + SelectOptionForFilter[] + >([]); + + useEffect(() => { + if (selectOptions) { + const options = selectOptions.map((option) => { + const isSelected = + objectFilterDropdownSelectedOptionValues?.includes(option.value) ?? + false; + + return { + ...option, + isSelected, + }; + }); + + setSelectableOptions(options); + } + }, [objectFilterDropdownSelectedOptionValues, selectOptions]); + + const handleMultipleOptionSelectChange = ( + optionChanged: SelectOptionForFilter, + isSelected: boolean, + ) => { + if (!selectOptions) { + return; + } + + const newSelectableOptions = selectableOptions.map((option) => + option.id === optionChanged.id ? { ...option, isSelected } : option, + ); + + setSelectableOptions(newSelectableOptions); + + const selectedOptions = newSelectableOptions.filter( + (option) => option.isSelected, + ); + + const filterDisplayValue = + selectedOptions.length > MAX_OPTIONS_TO_DISPLAY + ? `${selectedOptions.length} options` + : selectedOptions.map((option) => option.label).join(', '); + + if (filterDefinitionUsedInDropdown && selectedOperandInDropdown) { + const newFilterValue = + selectedOptions.length > 0 + ? JSON.stringify(selectedOptions.map((option) => option.value)) + : EMPTY_FILTER_VALUE; + + selectFilter({ + definition: filterDefinitionUsedInDropdown, + operand: selectedOperandInDropdown, + displayValue: filterDisplayValue, + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value: newFilterValue, + }); + } + }; + + const optionsInDropdown = selectableOptions?.filter((option) => + option.label.toLowerCase().includes(objectFilterDropdownSearchInput), + ); + + const showNoResult = optionsInDropdown?.length === 0; + + return ( + + {optionsInDropdown?.map((option) => ( + + handleMultipleOptionSelectChange(option, selected) + } + text={option.label} + className="" + /> + ))} + {showNoResult && } + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx similarity index 93% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx index 19db840d2..1a45f6aa3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx @@ -3,7 +3,7 @@ import { ChangeEvent } from 'react'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; -export const ObjectFilterDropdownRecordSearchInput = () => { +export const ObjectFilterDropdownSearchInput = () => { const { filterDefinitionUsedInDropdown, selectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index aa978a47d..8e8d52fe5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -13,8 +13,8 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; import { GenericEntityFilterChip } from './GenericEntityFilterChip'; -import { ObjectFilterDropdownRecordSearchInput } from './ObjectFilterDropdownEntitySearchInput'; import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; +import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput'; export const SingleEntityObjectFilterDropdownButton = ({ hotkeyScope, @@ -66,7 +66,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ } dropdownComponents={ <> - + diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index 2b4932cba..90d6f77f1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -27,6 +27,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setObjectFilterDropdownSelectedEntityId, objectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedOptionValues, + setObjectFilterDropdownSelectedOptionValues, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownUnfolded, @@ -87,6 +89,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setObjectFilterDropdownSelectedEntityId, objectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedOptionValues, + setObjectFilterDropdownSelectedOptionValues, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownUnfolded, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts index 3e6d25891..94b969b84 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts @@ -8,6 +8,7 @@ import { isObjectFilterDropdownOperandSelectUnfoldedScopedState } from '../state import { isObjectFilterDropdownUnfoldedScopedState } from '../states/isObjectFilterDropdownUnfoldedScopedState'; import { objectFilterDropdownSearchInputScopedState } from '../states/objectFilterDropdownSearchInputScopedState'; import { objectFilterDropdownSelectedEntityIdScopedState } from '../states/objectFilterDropdownSelectedEntityIdScopedState'; +import { objectFilterDropdownSelectedOptionValuesScopedState } from '../states/objectFilterDropdownSelectedOptionValuesScopedState'; import { selectedFilterScopedState } from '../states/selectedFilterScopedState'; import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; @@ -37,6 +38,14 @@ export const useFilterDropdownStates = (scopeId: string) => { scopeId, ); + const [ + objectFilterDropdownSelectedOptionValues, + setObjectFilterDropdownSelectedOptionValues, + ] = useRecoilScopedStateV2( + objectFilterDropdownSelectedOptionValuesScopedState, + scopeId, + ); + const [ isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, @@ -71,6 +80,8 @@ export const useFilterDropdownStates = (scopeId: string) => { objectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId, objectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedOptionValues, + setObjectFilterDropdownSelectedOptionValues, setObjectFilterDropdownSelectedRecordIds, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts new file mode 100644 index 000000000..8db977b57 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useOptionsForSelect.ts @@ -0,0 +1,26 @@ +import { useParams } from 'react-router-dom'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; + +export const DEFAULT_SEARCH_REQUEST_LIMIT = 60; + +export const useOptionsForSelect = (fieldMetadataId: string) => { + const objectNamePlural = useParams().objectNamePlural ?? ''; + + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + + const fieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + + const selectOptions = fieldMetadataItem?.options; + + return { + selectOptions, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts new file mode 100644 index 000000000..372da4c5d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts @@ -0,0 +1,7 @@ +import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap'; + +export const objectFilterDropdownSelectedOptionValuesScopedState = + createStateScopeMap({ + key: 'objectFilterDropdownSelectedOptionValuesScopedState', + defaultValue: [], + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts index 2c182f0a8..7ec43a222 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts @@ -7,4 +7,5 @@ export type FilterType = | 'CURRENCY' | 'FULL_NAME' | 'LINK' - | 'RELATION'; + | 'RELATION' + | 'SELECT'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index cd70c9334..94c6f92d5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -16,6 +16,7 @@ export const getOperandsForFilterType = ( case 'DATE_TIME': return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]; case 'RELATION': + case 'SELECT': return [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; default: return []; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index 4c730f429..94127f562 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -1,10 +1,13 @@ +import { useState } from 'react'; import styled from '@emotion/styled'; -import { MenuItem } from 'tsup.ui.index'; import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { MenuItemSelectTag } from '@/ui/navigation/menu-item/components/MenuItemSelectTag'; const StyledRelationPickerContainer = styled.div` left: -1px; @@ -17,16 +20,36 @@ export type SelectFieldInputProps = { }; export const SelectFieldInput = ({ onSubmit }: SelectFieldInputProps) => { - const { persistField, fieldDefinition } = useSelectField(); + const { persistField, fieldDefinition, fieldValue } = useSelectField(); + const [searchFilter, setSearchFilter] = useState(''); + + const selectedOption = fieldDefinition.metadata.options.find( + (option) => option.value === fieldValue, + ); + const optionsToSelect = + fieldDefinition.metadata.options.filter((option) => { + return option.value !== fieldValue && option.label.includes(searchFilter); + }) || []; + const optionsInDropDown = selectedOption + ? [selectedOption, ...optionsToSelect] + : optionsToSelect; return ( - - {fieldDefinition.metadata.options.map((option) => { + setSearchFilter(event.currentTarget.value)} + autoFocus + /> + + + {optionsInDropDown.map((option) => { return ( - onSubmit?.(() => persistField(option.value))} /> ); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 9a2ff1ff8..4eed2314b 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -141,6 +141,7 @@ export const isRecordMatchingFilter = ({ switch (objectMetadataField.type) { case FieldMetadataType.Email: case FieldMetadataType.Phone: + case FieldMetadataType.Select: case FieldMetadataType.Text: { return isMatchingStringFilter({ stringFilter: filterValue as StringFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index 352166c0a..ab0cbcbfa 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -1,3 +1,5 @@ +import { isNonEmptyString } from '@sniptt/guards'; + import { CurrencyFilter, DateFilter, @@ -254,6 +256,48 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( ); } break; + case 'SELECT': { + const stringifiedSelectValues = rawUIFilter.value; + let parsedOptionValues: string[] = []; + + if (!isNonEmptyString(stringifiedSelectValues)) { + break; + } + + try { + parsedOptionValues = JSON.parse(stringifiedSelectValues); + } catch (e) { + throw new Error( + `Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`, + ); + } + + if (parsedOptionValues.length > 0) { + switch (rawUIFilter.operand) { + case ViewFilterOperand.Is: + objectRecordFilters.push({ + [correspondingField.name]: { + in: parsedOptionValues, + } as UUIDFilter, + }); + break; + case ViewFilterOperand.IsNot: + objectRecordFilters.push({ + not: { + [correspondingField.name]: { + in: parsedOptionValues, + } as UUIDFilter, + }, + }); + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + ); + } + } + break; + } default: throw new Error('Unknown filter type'); } diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectTag.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectTag.tsx new file mode 100644 index 000000000..3038fa3a8 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectTag.tsx @@ -0,0 +1,40 @@ +import { useTheme } from '@emotion/react'; +import { Tag } from 'tsup.ui.index'; + +import { IconCheck } from '@/ui/display/icon'; +import { ThemeColor } from '@/ui/theme/constants/MainColorNames'; + +import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase'; + +import { StyledMenuItemSelect } from './MenuItemSelect'; + +type MenuItemSelectTagProps = { + selected: boolean; + className?: string; + onClick?: () => void; + color: ThemeColor; + text: string; +}; + +export const MenuItemSelectTag = ({ + color, + selected, + className, + onClick, + text, +}: MenuItemSelectTagProps) => { + const theme = useTheme(); + + return ( + + + + + {selected && } + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx index 991ed56c0..3a2727f48 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx @@ -26,6 +26,7 @@ export const ViewBarFilterEffect = ({ setOnFilterSelect, filterDefinitionUsedInDropdown, setObjectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedOptionValues, isObjectFilterDropdownUnfolded, } = useFilterDropdown({ filterDropdownId }); @@ -61,12 +62,29 @@ export const ViewBarFilterEffect = ({ : []; setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds); + } else if (filterDefinitionUsedInDropdown?.type === 'SELECT') { + const viewFilterUsedInDropdown = currentViewFilters.find( + (filter) => + filter.fieldMetadataId === + filterDefinitionUsedInDropdown.fieldMetadataId, + ); + + const viewFilterSelectedOptionValues = isNonEmptyString( + viewFilterUsedInDropdown?.value, + ) + ? JSON.parse(viewFilterUsedInDropdown.value) + : []; + + setObjectFilterDropdownSelectedOptionValues( + viewFilterSelectedOptionValues, + ); } }, [ filterDefinitionUsedInDropdown, currentViewFilters, setObjectFilterDropdownSelectedRecordIds, isObjectFilterDropdownUnfolded, + setObjectFilterDropdownSelectedOptionValues, ]); return <>; diff --git a/packages/twenty-server/src/workspace/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts b/packages/twenty-server/src/workspace/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts index c7eec0ac1..67b95c2ea 100644 --- a/packages/twenty-server/src/workspace/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts +++ b/packages/twenty-server/src/workspace/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts @@ -82,5 +82,41 @@ describe('ArgsStringFactory', () => { 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]', ); }); + + it('when orderBy is present with position criteria, should return position at the end of the list', () => { + const args = { + orderBy: { + position: 'AscNullsFirst', + id: 'AscNullsFirst', + name: 'AscNullsFirst', + }, + }; + + argsAliasCreate.mockReturnValue(args); + + const result = service.create(args, []); + + expect(result).toEqual( + 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]', + ); + }); + + it('when orderBy is present with position in the middle, should return position at the end of the list', () => { + const args = { + orderBy: { + id: 'AscNullsFirst', + position: 'AscNullsFirst', + name: 'AscNullsFirst', + }, + }; + + argsAliasCreate.mockReturnValue(args); + + const result = service.create(args, []); + + expect(result).toEqual( + 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]', + ); + }); }); }); diff --git a/packages/twenty-server/src/workspace/workspace-query-builder/factories/args-string.factory.ts b/packages/twenty-server/src/workspace/workspace-query-builder/factories/args-string.factory.ts index cb80f5b67..cabda5d55 100644 --- a/packages/twenty-server/src/workspace/workspace-query-builder/factories/args-string.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-query-builder/factories/args-string.factory.ts @@ -62,6 +62,9 @@ export class ArgsStringFactory { // PgGraphql is expecting the orderBy argument to be an array of objects if (key === 'orderBy') { const orderByString = Object.keys(obj) + .sort((_, b) => { + return b === 'position' ? -1 : 0; + }) .map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`) .join(', ');