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:
committed by
GitHub
parent
6e2df0654b
commit
b3e39e2198
+33
-10
@@ -5,7 +5,6 @@ import {
|
||||
addMonths,
|
||||
addSeconds,
|
||||
addWeeks,
|
||||
addYears,
|
||||
subDays,
|
||||
subHours,
|
||||
subMinutes,
|
||||
@@ -22,8 +21,6 @@ import {
|
||||
parseAndEvaluateRelativeDateFilter,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util';
|
||||
|
||||
// TODO: this test should be in twenty-shared, and the logic that is duplicated both front end and back end,
|
||||
// should be merged and properly refactored with Temporal to unify and simplify this bug-prone zone of the codebase.
|
||||
describe('Relative Date Filter Utils', () => {
|
||||
const now = new Date('2024-01-15T12:00:00Z'); // Monday, January 15, 2024 at noon
|
||||
|
||||
@@ -315,6 +312,13 @@ describe('Relative Date Filter Utils', () => {
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: new Date('2024-01-16T12:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for dates within the next N months', () => {
|
||||
@@ -351,39 +355,51 @@ describe('Relative Date Filter Utils', () => {
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: new Date('2024-01-20T12:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for dates within the next N years', () => {
|
||||
it('should match the next calendar year, not a rolling 12 months', () => {
|
||||
const relativeDateFilterValue: RelativeDateFilter = {
|
||||
direction: 'NEXT',
|
||||
amount: 1,
|
||||
unit: 'YEAR',
|
||||
};
|
||||
|
||||
// Dates within the next year should match
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: addMonths(now, 6),
|
||||
dateToCheck: new Date('2025-01-01T00:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: addMonths(now, 11),
|
||||
dateToCheck: new Date('2025-06-15T12:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
// Dates outside the range should not match
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: addYears(now, 2),
|
||||
dateToCheck: new Date('2024-07-15T12:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: subDays(now, 1),
|
||||
dateToCheck: new Date('2024-12-31T23:59:59Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: new Date('2026-01-01T00:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -607,6 +623,13 @@ describe('Relative Date Filter Utils', () => {
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
evaluateRelativeDateFilter({
|
||||
dateToCheck: new Date('2024-01-15T06:00:00Z'),
|
||||
relativeDateFilterValue,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for dates within the past N months', () => {
|
||||
|
||||
+28
-148
@@ -1,35 +1,11 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
import {
|
||||
endOfDay,
|
||||
endOfHour,
|
||||
endOfMinute,
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfSecond,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
isWithinInterval,
|
||||
startOfDay,
|
||||
startOfHour,
|
||||
startOfMinute,
|
||||
startOfMonth,
|
||||
startOfQuarter,
|
||||
startOfSecond,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
} from 'date-fns';
|
||||
import {
|
||||
addUnitToDateTime,
|
||||
assertUnreachable,
|
||||
getFirstDayOfTheWeekAsANumberForDateFNS,
|
||||
isDefined,
|
||||
type RelativeDateFilter,
|
||||
resolveRelativeDateTimeFilter,
|
||||
safeParseRelativeDateFilterJsonStringified,
|
||||
subUnitFromDateTime,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
// TODO: Merge this logic with resolveRelativeDateFilter in twenty-shared
|
||||
// But it is not urgent since we force all workflow filters to be in UTC
|
||||
export const parseAndEvaluateRelativeDateFilter = ({
|
||||
dateToCheck,
|
||||
relativeDateString,
|
||||
@@ -57,127 +33,31 @@ export const evaluateRelativeDateFilter = ({
|
||||
dateToCheck: Date;
|
||||
relativeDateFilterValue: RelativeDateFilter;
|
||||
}): boolean => {
|
||||
const now = new Date();
|
||||
|
||||
switch (relativeDateFilterValue.direction) {
|
||||
case 'NEXT':
|
||||
return evaluateNextDirection(dateToCheck, relativeDateFilterValue, now);
|
||||
case 'THIS':
|
||||
return evaluateThisDirection(dateToCheck, relativeDateFilterValue, now);
|
||||
case 'PAST':
|
||||
return evaluatePastDirection(dateToCheck, relativeDateFilterValue, now);
|
||||
default:
|
||||
return false;
|
||||
if (
|
||||
relativeDateFilterValue.direction !== 'THIS' &&
|
||||
!isDefined(relativeDateFilterValue.amount)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const referenceZonedDateTime = Temporal.Instant.fromEpochMilliseconds(
|
||||
new Date().getTime(),
|
||||
).toZonedDateTimeISO('UTC');
|
||||
|
||||
const { start, end } = resolveRelativeDateTimeFilter(
|
||||
relativeDateFilterValue,
|
||||
referenceZonedDateTime,
|
||||
);
|
||||
|
||||
const dateToCheckInstant = Temporal.Instant.fromEpochMilliseconds(
|
||||
dateToCheck.getTime(),
|
||||
);
|
||||
|
||||
// Half-open [start, end): the period end is exclusive (it is the start of the
|
||||
// next period), so a value that lands exactly on a boundary is only counted in
|
||||
// one of two adjacent periods.
|
||||
return (
|
||||
Temporal.Instant.compare(dateToCheckInstant, start.toInstant()) >= 0 &&
|
||||
Temporal.Instant.compare(dateToCheckInstant, end.toInstant()) < 0
|
||||
);
|
||||
};
|
||||
|
||||
const evaluateNextDirection = (
|
||||
dateToCheck: Date,
|
||||
relativeDateFilterValue: RelativeDateFilter,
|
||||
now: Date,
|
||||
): boolean => {
|
||||
if (!isDefined(relativeDateFilterValue.amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { amount, unit } = relativeDateFilterValue;
|
||||
|
||||
const endOfPeriod = addUnitToDateTime(now, amount, unit);
|
||||
|
||||
if (!endOfPeriod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: now,
|
||||
end: endOfPeriod,
|
||||
});
|
||||
};
|
||||
|
||||
function evaluatePastDirection(
|
||||
dateToCheck: Date,
|
||||
relativeDateFilterValue: RelativeDateFilter,
|
||||
now: Date,
|
||||
): boolean {
|
||||
if (!isDefined(relativeDateFilterValue.amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { amount, unit } = relativeDateFilterValue;
|
||||
|
||||
const startOfPeriod = subUnitFromDateTime(now, amount, unit);
|
||||
|
||||
if (!startOfPeriod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfPeriod,
|
||||
end: now,
|
||||
});
|
||||
}
|
||||
|
||||
function evaluateThisDirection(
|
||||
dateToCheck: Date,
|
||||
relativeDateValue: RelativeDateFilter,
|
||||
now: Date,
|
||||
): boolean {
|
||||
const { unit } = relativeDateValue;
|
||||
|
||||
const firstDayOfTheWeekAsANumberForDateFNS = isNonEmptyString(
|
||||
relativeDateValue.firstDayOfTheWeek,
|
||||
)
|
||||
? getFirstDayOfTheWeekAsANumberForDateFNS(
|
||||
relativeDateValue.firstDayOfTheWeek,
|
||||
)
|
||||
: 1;
|
||||
|
||||
switch (unit) {
|
||||
case 'SECOND':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfSecond(now),
|
||||
end: endOfSecond(now),
|
||||
});
|
||||
case 'MINUTE':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfMinute(now),
|
||||
end: endOfMinute(now),
|
||||
});
|
||||
case 'HOUR':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfHour(now),
|
||||
end: endOfHour(now),
|
||||
});
|
||||
case 'DAY':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfDay(now),
|
||||
end: endOfDay(now),
|
||||
});
|
||||
case 'WEEK':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfWeek(now, {
|
||||
weekStartsOn: firstDayOfTheWeekAsANumberForDateFNS,
|
||||
}),
|
||||
end: endOfWeek(now, {
|
||||
weekStartsOn: firstDayOfTheWeekAsANumberForDateFNS,
|
||||
}),
|
||||
});
|
||||
case 'MONTH':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfMonth(now),
|
||||
end: endOfMonth(now),
|
||||
});
|
||||
case 'YEAR':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfYear(now),
|
||||
end: endOfYear(now),
|
||||
});
|
||||
case 'QUARTER':
|
||||
return isWithinInterval(dateToCheck, {
|
||||
start: startOfQuarter(now),
|
||||
end: endOfQuarter(now),
|
||||
});
|
||||
default:
|
||||
return assertUnreachable(unit);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user