Follow-up high fixes on date refactor (#15553)

This PR fixes important bugs on date filter handling following-up date
refactor.

Fixes : https://github.com/twentyhq/core-team-issues/issues/1814
This commit is contained in:
Lucas Bordeau
2025-11-03 17:51:42 +01:00
committed by GitHub
parent bafc0496b1
commit b33b38cb02
16 changed files with 119 additions and 213 deletions
@@ -4,7 +4,7 @@ import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/uti
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useGetRecordFilterLabelValue } from '@/views/hooks/useGetRecordFilterLabelValue';
import { useGetRecordFilterChipLabelValue } from '@/views/hooks/useGetRecordFilterChipLabelValue';
import { isNonEmptyString } from '@sniptt/guards';
import { useIcons } from 'twenty-ui/display';
@@ -26,7 +26,7 @@ export const EditableFilterChip = ({
recordFilter.fieldMetadataId,
);
const { getRecordFilterLabelValue } = useGetRecordFilterLabelValue();
const { getRecordFilterChipLabelValue } = useGetRecordFilterChipLabelValue();
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
@@ -47,9 +47,8 @@ export const EditableFilterChip = ({
: recordFilter.label;
const labelKey = `${fieldNameLabel}`;
const labelValue = getRecordFilterLabelValue({
const labelValue = getRecordFilterChipLabelValue({
recordFilter,
fieldMetadataOptions: fieldMetadataItem.options ?? [],
});
return (
@@ -4,7 +4,7 @@ import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFiel
import { MAX_RECORDS_TO_DISPLAY } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
import { useGetRecordFilterLabelValue } from '@/views/hooks/useGetRecordFilterLabelValue';
import { useGetRecordFilterChipLabelValue } from '@/views/hooks/useGetRecordFilterChipLabelValue';
import { t } from '@lingui/core/macro';
import {
@@ -17,12 +17,13 @@ type ObjectFilterDropdownRecordSelectProps = {
recordFilter: RecordFilter;
};
// TODO: refactor this with new useGetRecordFilterDisplayValue
export const useComputeRecordRelationFilterLabelValue = ({
recordFilter,
}: ObjectFilterDropdownRecordSelectProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
const { getRecordFilterLabelValue } = useGetRecordFilterLabelValue();
const { getRecordFilterChipLabelValue } = useGetRecordFilterChipLabelValue();
if (!isDefined(recordFilter.fieldMetadataId)) {
throw new Error('fieldMetadataItemUsedInFilterDropdown is not defined');
@@ -87,13 +88,13 @@ export const useComputeRecordRelationFilterLabelValue = ({
return {
labelValue:
labelValueItems.length > 0
? getRecordFilterLabelValue({
? getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
displayValue: filterDisplayValue,
},
})
: getRecordFilterLabelValue({
: getRecordFilterChipLabelValue({
recordFilter,
}),
};
@@ -0,0 +1,33 @@
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { useGetRecordFilterDisplayValue } from '@/object-record/record-filter/hooks/useGetRecordFilterDisplayValue';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { isEmptinessOperand } from 'twenty-shared/utils';
export const useGetRecordFilterChipLabelValue = () => {
const { getRecordFilterDisplayValue } = useGetRecordFilterDisplayValue();
const getRecordFilterChipLabelValue = ({
recordFilter,
}: {
recordFilter: RecordFilter;
}) => {
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
const operandIsEmptiness = isEmptinessOperand(recordFilter.operand);
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
const recordFilterDisplayValue = getRecordFilterDisplayValue(recordFilter);
if (!operandIsEmptiness && !recordFilterIsEmpty) {
return `${operandLabelShort} ${recordFilterDisplayValue}`;
}
if (operandIsEmptiness) {
return `${operandLabelShort}`;
}
return recordFilterDisplayValue;
};
return { getRecordFilterChipLabelValue };
};
@@ -1,149 +0,0 @@
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { useGetDateTimeFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateTimeFilterDisplayValue';
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { UserContext } from '@/users/contexts/UserContext';
import { isValid } from 'date-fns';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import {
getDateFromPlainDate,
isEmptinessOperand,
parseJson,
relativeDateFilterStringifiedSchema,
shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone,
} from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateString } from '~/utils/string/formatDateString';
export const useGetRecordFilterLabelValue = () => {
const { dateFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const { userTimezone } = useUserTimezone();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const getRecordFilterLabelValue = ({
recordFilter,
fieldMetadataOptions,
}: {
recordFilter: RecordFilter;
fieldMetadataOptions?: FieldMetadataItemOption[];
}) => {
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
const operandIsEmptiness = isEmptinessOperand(recordFilter.operand);
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
if (recordFilter.type === 'DATE') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS: {
const date = getDateFromPlainDate(recordFilter.value);
const shiftedDate =
shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone(
date,
userTimezone,
'add',
);
if (!isValid(date)) {
return `${operandLabelShort}`;
}
const formattedDate = formatDateString({
value: shiftedDate.toISOString(),
timeZone,
dateFormat,
localeCatalog: dateLocale.localeCatalog,
});
return `${operandLabelShort} ${formattedDate}`;
}
case RecordFilterOperand.IS_RELATIVE: {
const relativeDateFilter =
relativeDateFilterStringifiedSchema.safeParse(recordFilter.value);
if (!relativeDateFilter.success) {
return `${operandLabelShort}`;
}
const relativeDateDisplayValue = getRelativeDateDisplayValue(
relativeDateFilter.data,
);
return `${operandLabelShort} ${relativeDateDisplayValue}`;
}
case RecordFilterOperand.IS_TODAY:
case RecordFilterOperand.IS_IN_FUTURE:
case RecordFilterOperand.IS_IN_PAST:
return operandLabelShort;
default:
return `${operandLabelShort} ${recordFilter.displayValue}`;
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
case RecordFilterOperand.IS_AFTER:
case RecordFilterOperand.IS_BEFORE: {
const pointInTime = new Date(recordFilter.value);
const { displayValue } = getDateTimeFilterDisplayValue(pointInTime);
return `${operandLabelShort} ${displayValue}`;
}
case RecordFilterOperand.IS_RELATIVE: {
const relativeDateFilter =
relativeDateFilterStringifiedSchema.safeParse(recordFilter.value);
if (!relativeDateFilter.success) {
return `${operandLabelShort}`;
}
const relativeDateDisplayValue = getRelativeDateDisplayValue(
relativeDateFilter.data,
);
return `${operandLabelShort} ${relativeDateDisplayValue}`;
}
case RecordFilterOperand.IS_TODAY:
case RecordFilterOperand.IS_IN_FUTURE:
case RecordFilterOperand.IS_IN_PAST:
return operandLabelShort;
default:
return `${operandLabelShort} ${recordFilter.displayValue}`;
}
} else if (
recordFilter.type === 'SELECT' ||
recordFilter.type === 'MULTI_SELECT'
) {
const valueArray = parseJson<string[]>(recordFilter.value);
if (!Array.isArray(valueArray)) {
return '';
}
const optionLabels = valueArray.map(
(value) =>
fieldMetadataOptions?.find((option) => option.value === value)?.label,
);
return `${operandLabelShort} ${optionLabels.join(', ')}`;
}
if (!operandIsEmptiness && !recordFilterIsEmpty) {
return `${operandLabelShort} ${recordFilter.displayValue}`;
}
if (operandIsEmptiness) {
return `${operandLabelShort}`;
}
return recordFilter.displayValue;
};
return { getRecordFilterLabelValue };
};
@@ -160,7 +160,7 @@ describe('stringifyRelativeDateFilter', () => {
});
describe('timezone handling', () => {
it('should append timezone when provided', () => {
it('should append timezone when provided and default first day of the week', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
@@ -169,7 +169,7 @@ describe('stringifyRelativeDateFilter', () => {
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;',
'PAST_5_DAY;;America/New_York;;MONDAY;;',
);
});
@@ -206,74 +206,18 @@ describe('stringifyRelativeDateFilter', () => {
});
});
describe('referenceDayAsString handling', () => {
it('should append referenceDayAsString when timezone is provided', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '2024-01-15',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;2024-01-15;;MONDAY;;',
);
});
it('should not append referenceDayAsString when timezone is not provided', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
referenceDayAsString: '2024-01-15',
};
expect(stringifyRelativeDateFilter(filter)).toBe('PAST_5_DAY');
});
it('should not append referenceDayAsString when it is empty', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;',
);
});
it('should not append referenceDayAsString when it is null', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: null,
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;',
);
});
});
describe('firstDayOfTheWeek handling', () => {
it('should append firstDayOfTheWeek when timezone and referenceDayAsString are provided', () => {
it('should append firstDayOfTheWeek when timezone is provided', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '2024-01-15',
firstDayOfTheWeek: 'SUNDAY',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;2024-01-15;;SUNDAY;;',
'PAST_5_DAY;;America/New_York;;SUNDAY;;',
);
});
@@ -285,11 +229,10 @@ describe('stringifyRelativeDateFilter', () => {
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '2024-01-15',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;2024-01-15;;MONDAY;;',
'PAST_5_DAY;;America/New_York;;MONDAY;;',
);
expect(mockDetectCalendarStartDay).toHaveBeenCalledTimes(1);
});
@@ -302,37 +245,20 @@ describe('stringifyRelativeDateFilter', () => {
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '2024-01-15',
firstDayOfTheWeek: null,
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;2024-01-15;;SATURDAY;;',
'PAST_5_DAY;;America/New_York;;SATURDAY;;',
);
expect(mockDetectCalendarStartDay).toHaveBeenCalledTimes(1);
});
it('should not append firstDayOfTheWeek when referenceDayAsString is not provided', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
firstDayOfTheWeek: 'SUNDAY',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;',
);
expect(mockDetectCalendarStartDay).not.toHaveBeenCalled();
});
it('should not append firstDayOfTheWeek when timezone is not provided', () => {
const filter: RelativeDateFilter = {
direction: 'PAST',
amount: 5,
unit: 'DAY',
referenceDayAsString: '2024-01-15',
firstDayOfTheWeek: 'SUNDAY',
};
@@ -348,11 +274,10 @@ describe('stringifyRelativeDateFilter', () => {
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
referenceDayAsString: '2024-01-15',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'PAST_5_DAY;;America/New_York;;2024-01-15;;',
'PAST_5_DAY;;America/New_York;;',
);
});
});
@@ -364,12 +289,11 @@ describe('stringifyRelativeDateFilter', () => {
amount: 10,
unit: 'MONTH',
timezone: 'Europe/London',
referenceDayAsString: '2024-12-25',
firstDayOfTheWeek: 'MONDAY',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'NEXT_10_MONTH;;Europe/London;;2024-12-25;;MONDAY;;',
'NEXT_10_MONTH;;Europe/London;;MONDAY;;',
);
});
@@ -379,12 +303,11 @@ describe('stringifyRelativeDateFilter', () => {
amount: 7,
unit: 'WEEK',
timezone: 'Asia/Tokyo',
referenceDayAsString: '2024-06-01',
firstDayOfTheWeek: 'SUNDAY',
};
expect(stringifyRelativeDateFilter(filter)).toBe(
'THIS_1_WEEK;;Asia/Tokyo;;2024-06-01;;SUNDAY;;',
'THIS_1_WEEK;;Asia/Tokyo;;SUNDAY;;',
);
});
@@ -21,15 +21,11 @@ export const stringifyRelativeDateFilter = (
if (isNonEmptyString(relativeDateFilter.timezone)) {
relativeDateFilterStringified = `${relativeDateFilterStringified};;${relativeDateFilter.timezone};;`;
if (isNonEmptyString(relativeDateFilter.referenceDayAsString)) {
relativeDateFilterStringified = `${relativeDateFilterStringified}${relativeDateFilter.referenceDayAsString};;`;
const firstDayOfTheWeek =
relativeDateFilter.firstDayOfTheWeek ?? detectCalendarStartDay();
const firstDayOfTheWeek =
relativeDateFilter.firstDayOfTheWeek ?? detectCalendarStartDay();
if (isNonEmptyString(firstDayOfTheWeek)) {
relativeDateFilterStringified = `${relativeDateFilterStringified}${firstDayOfTheWeek};;`;
}
if (isNonEmptyString(firstDayOfTheWeek)) {
relativeDateFilterStringified = `${relativeDateFilterStringified}${firstDayOfTheWeek};;`;
}
}