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
This commit is contained in:
Lucas Bordeau
2025-06-16 10:35:20 +02:00
committed by GitHub
parent 6d6738e7cb
commit a05c659e03
10 changed files with 662 additions and 179 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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<RecordFilter, 'value' | 'displayValue'> | Record<string, never> => {
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: '',
};
};

View File

@ -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,
};

View File

@ -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,

View File

@ -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' });
});
});

View File

@ -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 };
};

View File

@ -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 (

View File

@ -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 }) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
}}
>
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: 'test' }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'test' }}
>
{children}
</RecordFiltersComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecoilRoot>
);
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();
});
});

View File

@ -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,
};
};