fix: relative date picker calendar display (#21895)

Part of
https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526
(Bug 1-3). Maybe it feels like theses bugs are not actually bugs, but we
can maybe say it as UX improvements: specially needed in case when an
user will choose any past options.

### Bug 1: calendar open on wrong month
With Is Relative (e.g. Past 1 Quarter), the calendar opened on today’s
month instead of the range start. After the fix, it now opens on the
first month of the filtered range.

**Testing:**
View filter → Date field → Is Relative → Past 1 Quarter. Calendar opens
on January (range start), not today’s month


https://github.com/user-attachments/assets/8849d00a-4d5c-4f8a-8d31-3a62535eb311


### Bug 2: Dates not highlighted
Ranges older than ~2 months (e.g. Q1 when today is June) showed no
highlighted days. Highlighting now covers the full resolved range.

**Testing:**
Same setup: past 1 Quarter on a date when Q1 is outside the old 2‑month
window. Jan 1 - Mar 31 will highlight.


https://github.com/user-attachments/assets/d21e2272-c923-4493-80ff-bdf4228842b1


### Bug 3: No month navigation
Relative mode only showed Past - 1 - Quarter controls with no way to
browse months. Now see the new arrows move through months without
changing the filter.

<img width="377" height="455" alt="Screenshot 2026-06-20 181107"
src="https://github.com/user-attachments/assets/eb51feb9-af10-489a-b166-8b8d6c642e05"
/>


> [!NOTE]
> 1. We can't do the fixes by one by one, i have to fix them within one
PR because all the fixes are inter-related, like we can't test the bug 1
fix alone without implementing bug 3.
> 2. Bug 4 will be done in a separate PR which is actually the issue
#19739. See
https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526
for better understanding.
> 3. If you see the screen recordings, they are actually done with the
alignment fixes from #21881 . So without that changes you will see the
alignmemt issues in the calendar grid in your local.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Parship Chowdhury
2026-06-26 13:33:21 +05:30
committed by GitHub
parent 6e2df0654b
commit b3e39e2198
30 changed files with 969 additions and 662 deletions
@@ -1,5 +1,6 @@
import { Temporal } from 'temporal-polyfill';
import { FirstDayOfTheWeek } from '@/types';
import { resolveRelativeDateFilter } from '@/utils/filter/dates/utils/resolveRelativeDateFilter';
describe('resolveRelativeDateFilter', () => {
@@ -98,4 +99,81 @@ describe('resolveRelativeDateFilter', () => {
expect(result.end).toBe('2024-07-01');
});
});
describe('calendar-aligned PAST/NEXT for week/month/year', () => {
it('should compute PAST 1 WEEK as the previous calendar week', () => {
const result = resolveRelativeDateFilter(
{ direction: 'PAST', amount: 1, unit: 'WEEK' },
referenceZdt,
);
expect(result.start).toBe('2024-03-04');
expect(result.end).toBe('2024-03-11');
});
it('should compute NEXT 1 WEEK as the next calendar week', () => {
const result = resolveRelativeDateFilter(
{ direction: 'NEXT', amount: 1, unit: 'WEEK' },
referenceZdt,
);
expect(result.start).toBe('2024-03-18');
expect(result.end).toBe('2024-03-25');
});
it('should respect firstDayOfTheWeek when aligning PAST 1 WEEK', () => {
const result = resolveRelativeDateFilter(
{
direction: 'PAST',
amount: 1,
unit: 'WEEK',
firstDayOfTheWeek: FirstDayOfTheWeek.SUNDAY,
},
referenceZdt,
);
expect(result.start).toBe('2024-03-03');
expect(result.end).toBe('2024-03-10');
});
it('should compute PAST 1 MONTH as the previous calendar month', () => {
const result = resolveRelativeDateFilter(
{ direction: 'PAST', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start).toBe('2024-02-01');
expect(result.end).toBe('2024-03-01');
});
it('should compute NEXT 1 MONTH as the next calendar month', () => {
const result = resolveRelativeDateFilter(
{ direction: 'NEXT', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start).toBe('2024-04-01');
expect(result.end).toBe('2024-05-01');
});
it('should compute PAST 1 YEAR as the previous calendar year', () => {
const result = resolveRelativeDateFilter(
{ direction: 'PAST', amount: 1, unit: 'YEAR' },
referenceZdt,
);
expect(result.start).toBe('2023-01-01');
expect(result.end).toBe('2024-01-01');
});
it('should compute NEXT 1 YEAR as the next calendar year', () => {
const result = resolveRelativeDateFilter(
{ direction: 'NEXT', amount: 1, unit: 'YEAR' },
referenceZdt,
);
expect(result.start).toBe('2025-01-01');
expect(result.end).toBe('2026-01-01');
});
});
});
@@ -12,8 +12,8 @@ describe('resolveRelativeDateTimeFilter', () => {
referenceZdt,
);
expect(result.start).toEqual(referenceZdt);
expect(result.end?.hour).toBe(15);
expect(result.start?.hour).toBe(13);
expect(result.end?.hour).toBe(16);
});
it('should compute for DAY unit', () => {
@@ -105,4 +105,64 @@ describe('resolveRelativeDateTimeFilter', () => {
expect(result.end?.month).toBe(4);
});
});
describe('calendar-aligned PAST/NEXT for all units', () => {
it('should compute PAST 1 WEEK as the previous calendar week', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 1, unit: 'WEEK' },
referenceZdt,
);
expect(result.start?.month).toBe(3);
expect(result.start?.day).toBe(4);
expect(result.end?.month).toBe(3);
expect(result.end?.day).toBe(11);
});
it('should compute NEXT 1 WEEK as the next calendar week', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: 1, unit: 'WEEK' },
referenceZdt,
);
expect(result.start?.day).toBe(18);
expect(result.end?.day).toBe(25);
});
it('should compute PAST 1 MONTH as the previous calendar month', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start?.month).toBe(2);
expect(result.start?.day).toBe(1);
expect(result.end?.month).toBe(3);
expect(result.end?.day).toBe(1);
});
it('should compute NEXT 1 MONTH as the next calendar month', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start?.month).toBe(4);
expect(result.start?.day).toBe(1);
expect(result.end?.month).toBe(5);
expect(result.end?.day).toBe(1);
});
it('should align PAST 1 HOUR to the previous clock hour', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 1, unit: 'HOUR' },
Temporal.ZonedDateTime.from('2024-03-15T12:30:45[UTC]'),
);
expect(result.start?.hour).toBe(11);
expect(result.start?.minute).toBe(0);
expect(result.end?.hour).toBe(12);
expect(result.end?.minute).toBe(0);
});
});
});
@@ -0,0 +1,11 @@
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
const SUB_DAY_RELATIVE_DATE_FILTER_UNITS: readonly RelativeDateFilterUnit[] = [
'SECOND',
'MINUTE',
'HOUR',
];
export const isSubDayRelativeDateFilterUnit = (
unit: RelativeDateFilterUnit,
): boolean => SUB_DAY_RELATIVE_DATE_FILTER_UNITS.includes(unit);
@@ -6,7 +6,6 @@ import { subUnitFromZonedDateTime } from '@/utils/filter/dates/utils/subUnitFrom
import { isDefined } from 'class-validator';
import { type Temporal } from 'temporal-polyfill';
// TODO: use this in workflows where there is duplicated logic
export const resolveRelativeDateFilter = (
relativeDateFilter: RelativeDateFilter,
referenceTodayZonedDateTime: Temporal.ZonedDateTime,
@@ -19,52 +18,22 @@ export const resolveRelativeDateFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceTodayZonedDateTime,
'QUARTER',
firstDayOfTheWeek,
);
const startOfNextPeriod = getNextPeriodStart(
referenceTodayZonedDateTime,
unit,
firstDayOfTheWeek,
);
const startOfNextPeriod = addUnitToZonedDateTime(
startOfCurrentQuarter,
'QUARTER',
1,
);
const endOfNextPeriod = addUnitToZonedDateTime(
startOfNextPeriod,
'QUARTER',
amount,
);
const start = startOfNextPeriod.toPlainDate().toString();
const end = endOfNextPeriod.toPlainDate().toString();
return {
...relativeDateFilter,
start,
end,
};
}
const startOfNextDay = referenceTodayZonedDateTime
.startOfDay()
.add({ days: 1 });
const startOfNextPeriod = addUnitToZonedDateTime(
startOfNextDay,
const endOfNextPeriod = addUnitToZonedDateTime(
startOfNextPeriod,
unit,
amount,
);
const start = startOfNextDay.toPlainDate().toString();
const end = startOfNextPeriod?.toPlainDate().toString();
return {
...relativeDateFilter,
start,
end,
start: startOfNextPeriod.toPlainDate().toString(),
end: endOfNextPeriod.toPlainDate().toString(),
};
}
case 'PAST': {
@@ -72,44 +41,22 @@ export const resolveRelativeDateFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceTodayZonedDateTime,
'QUARTER',
firstDayOfTheWeek,
);
const startOfCurrentPeriod = getPeriodStart(
referenceTodayZonedDateTime,
unit,
firstDayOfTheWeek,
);
const startOfPastPeriod = subUnitFromZonedDateTime(
startOfCurrentQuarter,
'QUARTER',
amount,
);
const start = startOfPastPeriod.toPlainDate().toString();
const end = startOfCurrentQuarter.toPlainDate().toString();
return {
...relativeDateFilter,
start,
end,
};
}
const startOfDay = referenceTodayZonedDateTime.startOfDay();
const startOfNextPeriod = subUnitFromZonedDateTime(
startOfDay,
const startOfPastPeriod = subUnitFromZonedDateTime(
startOfCurrentPeriod,
unit,
amount,
);
const start = startOfNextPeriod?.toPlainDate().toString();
const end = startOfDay.toPlainDate().toString();
return {
...relativeDateFilter,
start,
end,
start: startOfPastPeriod.toPlainDate().toString(),
end: startOfCurrentPeriod.toPlainDate().toString(),
};
}
case 'THIS': {
@@ -12,78 +12,40 @@ export const resolveRelativeDateTimeFilter = (
) => {
const { direction, amount, unit, firstDayOfTheWeek } = relativeDateFilter;
const isSubDayUnit = ['SECOND', 'MINUTE', 'HOUR'].includes(unit);
switch (direction) {
case 'NEXT': {
if (!isDefined(amount)) {
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfNextQuarter = getNextPeriodStart(
referenceZonedDateTime,
'QUARTER',
);
const startOfNextPeriod = getNextPeriodStart(
referenceZonedDateTime,
unit,
firstDayOfTheWeek,
);
return {
...relativeDateFilter,
start: startOfNextQuarter,
end: addUnitToZonedDateTime(startOfNextQuarter, unit, amount),
};
}
if (isSubDayUnit) {
return {
...relativeDateFilter,
start: referenceZonedDateTime,
end: addUnitToZonedDateTime(referenceZonedDateTime, unit, amount),
};
} else {
const startOfNextDay = referenceZonedDateTime
.startOfDay()
.add({ days: 1 });
return {
...relativeDateFilter,
start: startOfNextDay,
end: addUnitToZonedDateTime(startOfNextDay, unit, amount),
};
}
return {
...relativeDateFilter,
start: startOfNextPeriod,
end: addUnitToZonedDateTime(startOfNextPeriod, unit, amount),
};
}
case 'PAST': {
if (!isDefined(amount)) {
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceZonedDateTime,
'QUARTER',
);
const startOfCurrentPeriod = getPeriodStart(
referenceZonedDateTime,
unit,
firstDayOfTheWeek,
);
return {
...relativeDateFilter,
start: subUnitFromZonedDateTime(startOfCurrentQuarter, unit, amount),
end: startOfCurrentQuarter,
};
}
if (isSubDayUnit) {
return {
...relativeDateFilter,
start: subUnitFromZonedDateTime(referenceZonedDateTime, unit, amount),
end: referenceZonedDateTime,
};
} else {
const startOfDay = referenceZonedDateTime.startOfDay();
return {
...relativeDateFilter,
start: subUnitFromZonedDateTime(startOfDay, unit, amount),
end: startOfDay,
};
}
return {
...relativeDateFilter,
start: subUnitFromZonedDateTime(startOfCurrentPeriod, unit, amount),
end: startOfCurrentPeriod,
};
}
case 'THIS':
return {
@@ -87,6 +87,7 @@ export {
getNextPeriodStart,
} from './filter/dates/utils/getNextPeriodStart';
export { getPeriodStart } from './filter/dates/utils/getPeriodStart';
export { isSubDayRelativeDateFilterUnit } from './filter/dates/utils/isSubDayRelativeDateFilterUnit';
export { relativeDateFilterAmountSchema } from './filter/dates/utils/relativeDateFilterAmountSchema';
export type { RelativeDateFilterDirection } from './filter/dates/utils/relativeDateFilterDirectionSchema';
export { relativeDateFilterDirectionSchema } from './filter/dates/utils/relativeDateFilterDirectionSchema';