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:
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: '',
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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' });
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user