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
+78
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+62
-2
@@ -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': {
|
||||
|
||||
+20
-58
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user