From a05c659e03b3087ec6f9a2941ac1ec7e66a00cf0 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 16 Jun 2025 10:35:20 +0200 Subject: [PATCH] Improved date filter input behavior (#12596) Opening a date picker when creating a filter now directly applies today's date to avoid any in-between state issues. This allows the date picker and the operand selection to behave nicely when creating a date filter. Fixes https://github.com/twentyhq/core-team-issues/issues/1049 --- ...useApplyObjectFilterDropdownFilterValue.ts | 93 ++--- ...UpsertObjectFilterDropdownCurrentFilter.ts | 24 +- .../utils/getInitialFilterValue.ts | 17 +- ...eEmptyRecordFilterFromFieldMetadataItem.ts | 10 +- ...erFromObjectFilterDropdownCurrentStates.ts | 107 +++--- .../getDateFilterDisplayValue.spec.ts | 31 ++ .../utils/getDateFilterDisplayValue.ts | 13 + ...ewBarFilterDropdownFieldSelectMenuItem.tsx | 83 +---- ...temFromViewBarFilterDropdown.test.test.tsx | 318 ++++++++++++++++++ ...ldMetadataItemFromViewBarFilterDropdown.ts | 145 ++++++++ 10 files changed, 662 insertions(+), 179 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/getDateFilterDisplayValue.spec.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/getDateFilterDisplayValue.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/__tests__/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.test.test.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue.ts index c94d6a961..bdc26e974 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue.ts @@ -3,21 +3,20 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; import { useCreateRecordFilterFromObjectFilterDropdownCurrentStates } from '@/object-record/record-filter/hooks/useCreateRecordFilterFromObjectFilterDropdownCurrentStates'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useApplyObjectFilterDropdownFilterValue = () => { - const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValueV2( - objectFilterDropdownCurrentRecordFilterComponentState, - ); + const objectFilterDropdownCurrentRecordFilterCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); - const objectFilterDropdownFilterNotYetCreated = !isDefined( - objectFilterDropdownCurrentRecordFilter, - ); - - const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( - fieldMetadataItemUsedInDropdownComponentSelector, - ); + const fieldMetadataItemUsedInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); const { createRecordFilterFromObjectFilterDropdownCurrentStates } = useCreateRecordFilterFromObjectFilterDropdownCurrentStates(); @@ -25,39 +24,55 @@ export const useApplyObjectFilterDropdownFilterValue = () => { const { upsertObjectFilterDropdownCurrentFilter } = useUpsertObjectFilterDropdownCurrentFilter(); - const applyObjectFilterDropdownFilterValue = ( - newFilterValue: string, - newDisplayValue?: string, - ) => { - if (objectFilterDropdownFilterNotYetCreated) { - if (!isDefined(fieldMetadataItemUsedInDropdown)) { - throw new Error( - `Field metadata item is not defined in object filter dropdown when setting a filter value to create it, this should not happen.`, - ); - } + const applyObjectFilterDropdownFilterValue = useRecoilCallback( + ({ snapshot }) => + (newFilterValue: string, newDisplayValue?: string) => { + const objectFilterDropdownCurrentRecordFilter = snapshot + .getLoadable(objectFilterDropdownCurrentRecordFilterCallbackState) + .getValue(); - const { newRecordFilterFromObjectFilterDropdownStates } = - createRecordFilterFromObjectFilterDropdownCurrentStates( - fieldMetadataItemUsedInDropdown, + const fieldMetadataItemUsedInDropdown = snapshot + .getLoadable(fieldMetadataItemUsedInDropdownCallbackState) + .getValue(); + + const objectFilterDropdownFilterNotYetCreated = !isDefined( + objectFilterDropdownCurrentRecordFilter, ); - const newCurrentRecordFilter = { - ...newRecordFilterFromObjectFilterDropdownStates, - value: newFilterValue, - displayValue: newDisplayValue ?? newFilterValue, - } satisfies RecordFilter; + if (objectFilterDropdownFilterNotYetCreated) { + if (!isDefined(fieldMetadataItemUsedInDropdown)) { + throw new Error( + `Field metadata item is not defined in object filter dropdown when setting a filter value to create it, this should not happen.`, + ); + } - upsertObjectFilterDropdownCurrentFilter(newCurrentRecordFilter); - } else { - const newCurrentRecordFilter = { - ...objectFilterDropdownCurrentRecordFilter, - value: newFilterValue, - displayValue: newDisplayValue ?? newFilterValue, - } satisfies RecordFilter; + const { newRecordFilterFromObjectFilterDropdownStates } = + createRecordFilterFromObjectFilterDropdownCurrentStates(); - upsertObjectFilterDropdownCurrentFilter(newCurrentRecordFilter); - } - }; + const newCurrentRecordFilter = { + ...newRecordFilterFromObjectFilterDropdownStates, + value: newFilterValue, + displayValue: newDisplayValue ?? newFilterValue, + } satisfies RecordFilter; + + upsertObjectFilterDropdownCurrentFilter(newCurrentRecordFilter); + } else { + const newCurrentRecordFilter = { + ...objectFilterDropdownCurrentRecordFilter, + value: newFilterValue, + displayValue: newDisplayValue ?? newFilterValue, + } satisfies RecordFilter; + + upsertObjectFilterDropdownCurrentFilter(newCurrentRecordFilter); + } + }, + [ + objectFilterDropdownCurrentRecordFilterCallbackState, + fieldMetadataItemUsedInDropdownCallbackState, + createRecordFilterFromObjectFilterDropdownCurrentStates, + upsertObjectFilterDropdownCurrentFilter, + ], + ); return { applyObjectFilterDropdownFilterValue, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter.ts index e3009382f..9438600c0 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter.ts @@ -1,23 +1,29 @@ import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; export const useUpsertObjectFilterDropdownCurrentFilter = () => { - const setObjectFilterDropdownCurrentRecordFilter = - useSetRecoilComponentStateV2( + const objectFilterDropdownCurrentRecordFilterCallbackState = + useRecoilComponentCallbackStateV2( objectFilterDropdownCurrentRecordFilterComponentState, ); const { upsertRecordFilter } = useUpsertRecordFilter(); - const upsertObjectFilterDropdownCurrentFilter = ( - recordFilterToUpsert: RecordFilter, - ) => { - upsertRecordFilter(recordFilterToUpsert); + const upsertObjectFilterDropdownCurrentFilter = useRecoilCallback( + ({ set }) => + (recordFilterToUpsert: RecordFilter) => { + upsertRecordFilter(recordFilterToUpsert); - setObjectFilterDropdownCurrentRecordFilter(recordFilterToUpsert); - }; + set( + objectFilterDropdownCurrentRecordFilterCallbackState, + recordFilterToUpsert, + ); + }, + [objectFilterDropdownCurrentRecordFilterCallbackState, upsertRecordFilter], + ); return { upsertObjectFilterDropdownCurrentFilter, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts index ab6752897..6d2f485bf 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts @@ -1,13 +1,11 @@ import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; -import { z } from 'zod'; +import { getDateFilterDisplayValue } from '@/object-record/record-filter/utils/getDateFilterDisplayValue'; export const getInitialFilterValue = ( newType: FilterableAndTSVectorFieldType, newOperand: RecordFilterOperand, - oldValue?: string, - oldDisplayValue?: string, ): Pick | Record => { switch (newType) { case 'DATE': @@ -19,12 +17,10 @@ export const getInitialFilterValue = ( ]; if (activeDatePickerOperands.includes(newOperand)) { - const date = z.coerce.date().safeParse(oldValue).data ?? new Date(); + const date = new Date(); const value = date.toISOString(); - const displayValue = - newType === 'DATE' - ? date.toLocaleString() - : date.toLocaleDateString(); + + const { displayValue } = getDateFilterDisplayValue(date, newType); return { value, displayValue }; } @@ -32,12 +28,13 @@ export const getInitialFilterValue = ( if (newOperand === RecordFilterOperand.IsRelative) { return { value: '', displayValue: '' }; } + break; } } return { - value: oldValue ?? '', - displayValue: oldDisplayValue ?? '', + value: '', + displayValue: '', }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts index f3bf9d98d..76bee8710 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem.ts @@ -1,5 +1,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; @@ -20,14 +21,19 @@ export const useCreateEmptyRecordFilterFromFieldMetadataItem = () => { const defaultSubFieldName = getDefaultSubFieldNameForCompositeFilterableFieldType(filterType); + const { displayValue, value } = getInitialFilterValue( + filterType, + defaultOperand, + ); + const newRecordFilter: RecordFilter = { id: v4(), fieldMetadataId: fieldMetadataItem.id, operand: defaultOperand, - displayValue: '', + displayValue, label: fieldMetadataItem.label, type: filterType, - value: '', + value, subFieldName: defaultSubFieldName, }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateRecordFilterFromObjectFilterDropdownCurrentStates.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateRecordFilterFromObjectFilterDropdownCurrentStates.ts index 7020e0f38..8ab0a78a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateRecordFilterFromObjectFilterDropdownCurrentStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCreateRecordFilterFromObjectFilterDropdownCurrentStates.ts @@ -1,59 +1,82 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; export const useCreateRecordFilterFromObjectFilterDropdownCurrentStates = () => { - const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( - fieldMetadataItemUsedInDropdownComponentSelector, - ); - - const selectedOperandInDropdown = useRecoilComponentValueV2( - selectedOperandInDropdownComponentState, - ); - - const subFieldNameUsedInDropdown = useRecoilComponentValueV2( - subFieldNameUsedInDropdownComponentState, - ); - - const createRecordFilterFromObjectFilterDropdownCurrentStates = ( - fieldMetadataItem: FieldMetadataItem, - ) => { - if (!isDefined(fieldMetadataItemUsedInDropdown)) { - throw new Error( - `Field metadata item used in dropdown is not defined when creating a record filter from object filter dropdown current states, this should not happen.`, - ); - } - - const filterType = getFilterTypeFromFieldType( - fieldMetadataItemUsedInDropdown.type, + const fieldMetadataItemUsedInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + fieldMetadataItemUsedInDropdownComponentSelector, ); - if (!isDefined(selectedOperandInDropdown)) { - throw new Error( - `Selected operand in dropdown is not defined when creating a record filter from object filter dropdown current states, this should not happen.`, - ); - } + const selectedOperandInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + selectedOperandInDropdownComponentState, + ); - const newRecordFilterFromObjectFilterDropdownStates: RecordFilter = { - id: v4(), - fieldMetadataId: fieldMetadataItemUsedInDropdown?.id, - operand: selectedOperandInDropdown, - displayValue: '', - label: fieldMetadataItem.label, - type: filterType, - value: '', - subFieldName: subFieldNameUsedInDropdown, - }; + const subFieldNameUsedInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + subFieldNameUsedInDropdownComponentState, + ); - return { newRecordFilterFromObjectFilterDropdownStates }; - }; + const createRecordFilterFromObjectFilterDropdownCurrentStates = + useRecoilCallback( + ({ snapshot }) => + () => { + const fieldMetadataItemUsedInDropdown = snapshot + .getLoadable(fieldMetadataItemUsedInDropdownCallbackState) + .getValue(); + + const selectedOperandInDropdown = snapshot + .getLoadable(selectedOperandInDropdownCallbackState) + .getValue(); + + const subFieldNameUsedInDropdown = snapshot + .getLoadable(subFieldNameUsedInDropdownCallbackState) + .getValue(); + + if (!isDefined(fieldMetadataItemUsedInDropdown)) { + throw new Error( + `Field metadata item used in dropdown is not defined when creating a record filter from object filter dropdown current states, this should not happen.`, + ); + } + + const filterType = getFilterTypeFromFieldType( + fieldMetadataItemUsedInDropdown.type, + ); + + if (!isDefined(selectedOperandInDropdown)) { + throw new Error( + `Selected operand in dropdown is not defined when creating a record filter from object filter dropdown current states, this should not happen.`, + ); + } + + const newRecordFilterFromObjectFilterDropdownStates: RecordFilter = + { + id: v4(), + fieldMetadataId: fieldMetadataItemUsedInDropdown.id, + operand: selectedOperandInDropdown, + displayValue: '', + label: fieldMetadataItemUsedInDropdown.label, + type: filterType, + value: '', + subFieldName: subFieldNameUsedInDropdown, + }; + + return { newRecordFilterFromObjectFilterDropdownStates }; + }, + [ + fieldMetadataItemUsedInDropdownCallbackState, + selectedOperandInDropdownCallbackState, + subFieldNameUsedInDropdownCallbackState, + ], + ); return { createRecordFilterFromObjectFilterDropdownCurrentStates, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/getDateFilterDisplayValue.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/getDateFilterDisplayValue.spec.ts new file mode 100644 index 000000000..d4a1ad811 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/getDateFilterDisplayValue.spec.ts @@ -0,0 +1,31 @@ +import { getDateFilterDisplayValue } from '../getDateFilterDisplayValue'; + +describe('getDateFilterDisplayValue', () => { + beforeAll(() => { + const mockDate = new Date('2025-06-13T14:30:00Z'); + + // Mocking responses for date methods to avoid timezone issues + jest + .spyOn(mockDate, 'toLocaleString') + .mockReturnValue('6/13/2025, 2:30:00 PM'); + jest.spyOn(mockDate, 'toLocaleDateString').mockReturnValue('6/13/2025'); + + global.Date = jest.fn(() => mockDate) as any; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should return date and time for DATE_TIME field type', () => { + const date = new Date('2025-06-13T14:30:00Z'); + const result = getDateFilterDisplayValue(date, 'DATE_TIME'); + expect(result).toEqual({ displayValue: '6/13/2025, 2:30:00 PM' }); + }); + + it('should return only date for DATE field type', () => { + const date = new Date('2025-06-13T14:30:00Z'); + const result = getDateFilterDisplayValue(date, 'DATE'); + expect(result).toEqual({ displayValue: '6/13/2025' }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getDateFilterDisplayValue.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDateFilterDisplayValue.ts new file mode 100644 index 000000000..0ed7d8e00 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getDateFilterDisplayValue.ts @@ -0,0 +1,13 @@ +import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; + +export const getDateFilterDisplayValue = ( + value: Date, + fieldType: FilterableAndTSVectorFieldType, +) => { + const displayValue = + fieldType === 'DATE_TIME' + ? value.toLocaleString() + : value.toLocaleDateString(); + + return { displayValue }; +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx index 3d6f1c513..6231e999a 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenuItem.tsx @@ -1,25 +1,10 @@ -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 { useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown } from '@/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown'; import { useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; @@ -30,14 +15,6 @@ export type ViewBarFilterDropdownFieldSelectMenuItemProps = { export const ViewBarFilterDropdownFieldSelectMenuItem = ({ fieldMetadataItemToSelect, }: ViewBarFilterDropdownFieldSelectMenuItemProps) => { - const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( - fieldMetadataItemIdUsedInDropdownComponentState, - ); - - const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2( - objectFilterDropdownFilterIsSelectedComponentState, - ); - const { resetSelectedItem } = useSelectableList(FILTER_FIELD_LIST_ID); const isSelectedItem = useRecoilComponentFamilyValueV2( @@ -45,58 +22,8 @@ export const ViewBarFilterDropdownFieldSelectMenuItem = ({ 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 { initializeFilterOnFieldMetataItemFromViewBarFilterDropdown } = + useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown(); const { getIcon } = useIcons(); @@ -105,7 +32,9 @@ export const ViewBarFilterDropdownFieldSelectMenuItem = ({ const handleClick = () => { resetSelectedItem(); - handleSelectFilter(fieldMetadataItemToSelect); + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown( + fieldMetadataItemToSelect, + ); }; return ( diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.test.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.test.test.tsx new file mode 100644 index 000000000..77d558b12 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.test.test.tsx @@ -0,0 +1,318 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown } from '../useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; +import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState'; +import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { getMockPersonObjectMetadataItem } from '~/testing/mock-data/people'; + +const mockSetHotkeyScope = jest.fn(); + +jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({ + useSetHotkeyScope: () => mockSetHotkeyScope, +})); + +const peopleObjectMetadataItemMock = getMockPersonObjectMetadataItem(); +const personCityFieldMetadataItemMock = + peopleObjectMetadataItemMock.fields.find((field) => field.name === 'city'); +const personCompanyFieldMetadataItemMock = + peopleObjectMetadataItemMock.fields.find((field) => field.name === 'company'); +const personCreatedAtFieldMetadataItemMock = + peopleObjectMetadataItemMock.fields.find( + (field) => field.name === 'createdAt', + ); + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + { + snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); + }} + > + + + {children} + + + +); + +describe('useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize filter for a basic text field with no existing filter', () => { + const { result } = renderHook( + () => { + const { initializeFilterOnFieldMetataItemFromViewBarFilterDropdown } = + useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown(); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + const objectFilterDropdownFilterIsSelected = useRecoilComponentValueV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + return { + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown, + fieldMetadataItemUsedInDropdown, + objectFilterDropdownFilterIsSelected, + selectedOperandInDropdown, + }; + }, + { + wrapper, + }, + ); + + if (!personCityFieldMetadataItemMock) { + throw new Error('personCityFieldMetadataItemMock is not defined'); + } + + const defaultOperand = getRecordFilterOperands({ + filterType: getFilterTypeFromFieldType( + personCityFieldMetadataItemMock.type, + ), + })?.[0]; + + act(() => { + result.current.initializeFilterOnFieldMetataItemFromViewBarFilterDropdown( + personCityFieldMetadataItemMock, + ); + }); + + expect(result.current.fieldMetadataItemUsedInDropdown?.id).toBe( + personCityFieldMetadataItemMock.id, + ); + expect(result.current.objectFilterDropdownFilterIsSelected).toBe(true); + expect(result.current.selectedOperandInDropdown).toBe(defaultOperand); + expect(mockSetHotkeyScope).not.toHaveBeenCalled(); + }); + + it('should initialize filter with a relation field', () => { + const { result } = renderHook( + () => { + const { initializeFilterOnFieldMetataItemFromViewBarFilterDropdown } = + useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown(); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + const objectFilterDropdownFilterIsSelected = useRecoilComponentValueV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + return { + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown, + fieldMetadataItemUsedInDropdown, + objectFilterDropdownFilterIsSelected, + selectedOperandInDropdown, + }; + }, + { + wrapper, + }, + ); + + if (!personCompanyFieldMetadataItemMock) { + throw new Error('personCompanyFieldMetadataItemMock is not defined'); + } + + const defaultOperand = getRecordFilterOperands({ + filterType: getFilterTypeFromFieldType( + personCompanyFieldMetadataItemMock.type, + ), + })?.[0]; + + act(() => { + result.current.initializeFilterOnFieldMetataItemFromViewBarFilterDropdown( + personCompanyFieldMetadataItemMock, + ); + }); + + expect(result.current.fieldMetadataItemUsedInDropdown?.id).toBe( + personCompanyFieldMetadataItemMock.id, + ); + expect(result.current.objectFilterDropdownFilterIsSelected).toBe(true); + expect(result.current.selectedOperandInDropdown).toBe(defaultOperand); + expect(mockSetHotkeyScope).toHaveBeenCalledWith( + SingleRecordPickerHotkeyScope.SingleRecordPicker, + ); + }); + + it('should initialize filter with a duplicate field on city', () => { + const { result } = renderHook( + () => { + const { initializeFilterOnFieldMetataItemFromViewBarFilterDropdown } = + useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown(); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + const objectFilterDropdownFilterIsSelected = useRecoilComponentValueV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const objectFilterDropdownCurrentRecordFilter = + useRecoilComponentValueV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const setCurrentRecordFilters = useSetRecoilComponentStateV2( + currentRecordFiltersComponentState, + ); + + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + return { + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown, + fieldMetadataItemUsedInDropdown, + objectFilterDropdownFilterIsSelected, + selectedOperandInDropdown, + setCurrentRecordFilters, + currentRecordFilters, + objectFilterDropdownCurrentRecordFilter, + }; + }, + { + wrapper, + }, + ); + + if (!personCityFieldMetadataItemMock) { + throw new Error('personCityFieldMetadataItemMock is not defined'); + } + + const defaultOperand = getRecordFilterOperands({ + filterType: getFilterTypeFromFieldType( + personCityFieldMetadataItemMock.type, + ), + })?.[0]; + + const mockExistingFilterOnCity: RecordFilter = { + id: 'existing-filter-id', + fieldMetadataId: personCityFieldMetadataItemMock.id, + operand: defaultOperand, + displayValue: 'Test City', + label: personCityFieldMetadataItemMock.label, + type: getFilterTypeFromFieldType(personCityFieldMetadataItemMock.type), + value: '', + }; + + act(() => { + result.current.setCurrentRecordFilters([mockExistingFilterOnCity]); + }); + + expect(result.current.currentRecordFilters.length).toBe(1); + + act(() => { + result.current.initializeFilterOnFieldMetataItemFromViewBarFilterDropdown( + personCityFieldMetadataItemMock, + ); + }); + + expect(result.current.objectFilterDropdownCurrentRecordFilter).toBe( + mockExistingFilterOnCity, + ); + }); + + it('should initialize filter on a date field correctly', () => { + const { result } = renderHook( + () => { + const { initializeFilterOnFieldMetataItemFromViewBarFilterDropdown } = + useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown(); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + const objectFilterDropdownFilterIsSelected = useRecoilComponentValueV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const objectFilterDropdownCurrentRecordFilter = + useRecoilComponentValueV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const setCurrentRecordFilters = useSetRecoilComponentStateV2( + currentRecordFiltersComponentState, + ); + + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + return { + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown, + fieldMetadataItemUsedInDropdown, + objectFilterDropdownFilterIsSelected, + selectedOperandInDropdown, + setCurrentRecordFilters, + currentRecordFilters, + objectFilterDropdownCurrentRecordFilter, + }; + }, + { + wrapper, + }, + ); + + if (!personCreatedAtFieldMetadataItemMock) { + throw new Error('personCreatedAtFieldMetadataItemMock is not defined'); + } + + expect(result.current.currentRecordFilters.length).toBe(0); + + act(() => { + result.current.initializeFilterOnFieldMetataItemFromViewBarFilterDropdown( + personCreatedAtFieldMetadataItemMock, + ); + }); + + expect(result.current.fieldMetadataItemUsedInDropdown?.id).toBe( + personCreatedAtFieldMetadataItemMock.id, + ); + + expect( + result.current.objectFilterDropdownCurrentRecordFilter?.fieldMetadataId, + ).toBe(personCreatedAtFieldMetadataItemMock.id); + + expect( + result.current.objectFilterDropdownCurrentRecordFilter?.value, + ).toBeDefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.ts b/packages/twenty-front/src/modules/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.ts new file mode 100644 index 000000000..09e41497c --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown.ts @@ -0,0 +1,145 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { useUpsertObjectFilterDropdownCurrentFilter } from '@/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter'; +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 { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; +import { getDateFilterDisplayValue } from '@/object-record/record-filter/utils/getDateFilterDisplayValue'; +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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +export const useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown = + () => { + const selectedOperandInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + selectedOperandInDropdownComponentState, + ); + + const setHotkeyScope = useSetHotkeyScope(); + + const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2( + currentRecordFiltersComponentState, + ); + + const objectFilterDropdownCurrentRecordFilterCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownCurrentRecordFilterComponentState, + ); + + const fieldMetadataItemUsedInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + + const objectFilterDropdownFilterIsSelectedCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownFilterIsSelectedComponentState, + ); + + const { upsertObjectFilterDropdownCurrentFilter } = + useUpsertObjectFilterDropdownCurrentFilter(); + + const initializeFilterOnFieldMetataItemFromViewBarFilterDropdown = + useRecoilCallback( + ({ set, snapshot }) => + (fieldMetadataItem: FieldMetadataItem) => { + set( + fieldMetadataItemUsedInDropdownCallbackState, + fieldMetadataItem.id, + ); + + const currentRecordFilters = snapshot + .getLoadable(currentRecordFiltersCallbackState) + .getValue(); + + const filterType = getFilterTypeFromFieldType( + fieldMetadataItem.type, + ); + + if (filterType === 'RELATION' || filterType === 'SELECT') { + setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker); + } + + set(objectFilterDropdownFilterIsSelectedCallbackState, true); + + const defaultOperand = getRecordFilterOperands({ + filterType, + })[0]; + + const duplicateFilterInCurrentRecordFilters = + findDuplicateRecordFilterInNonAdvancedRecordFilters({ + recordFilters: currentRecordFilters, + fieldMetadataItemId: fieldMetadataItem.id, + }); + + const filterIsAlreadyInCurrentRecordFilters = isDefined( + duplicateFilterInCurrentRecordFilters, + ); + + if (filterIsAlreadyInCurrentRecordFilters) { + set( + objectFilterDropdownCurrentRecordFilterCallbackState, + duplicateFilterInCurrentRecordFilters, + ); + + set( + selectedOperandInDropdownCallbackState, + duplicateFilterInCurrentRecordFilters.operand, + ); + } else { + set(selectedOperandInDropdownCallbackState, defaultOperand); + + if (filterType === 'DATE' || filterType === 'DATE_TIME') { + const date = new Date(); + const value = date.toISOString(); + + const { displayValue } = getDateFilterDisplayValue( + date, + filterType, + ); + + const initialDateRecordFilter: RecordFilter = { + id: v4(), + fieldMetadataId: fieldMetadataItem.id, + operand: defaultOperand, + displayValue, + label: fieldMetadataItem.label, + type: filterType, + value, + }; + + upsertObjectFilterDropdownCurrentFilter( + initialDateRecordFilter, + ); + + set( + objectFilterDropdownCurrentRecordFilterCallbackState, + initialDateRecordFilter, + ); + } + } + }, + [ + fieldMetadataItemUsedInDropdownCallbackState, + objectFilterDropdownCurrentRecordFilterCallbackState, + selectedOperandInDropdownCallbackState, + setHotkeyScope, + objectFilterDropdownFilterIsSelectedCallbackState, + currentRecordFiltersCallbackState, + upsertObjectFilterDropdownCurrentFilter, + ], + ); + + return { + initializeFilterOnFieldMetataItemFromViewBarFilterDropdown, + }; + };