feat : Added "Quarter" as a time unity to filter on date (#18289)

fixes #16674 

Simply added the Quarter functionality to the existing filter code with
months amount multiplied by 3.

<img width="1258" height="760" alt="Screenshot 2026-02-27 at 11 59
26 AM"
src="https://github.com/user-attachments/assets/30648259-2b72-480a-b8fa-55ed88d9ebf3"
/>

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Arun
2026-03-10 22:12:10 +05:30
committed by GitHub
parent e7fe435f60
commit 40a8d18d38
13 changed files with 196 additions and 4 deletions
@@ -9,5 +9,6 @@ export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnitOption[] = [
{ value: 'DAY', label: 'Day' },
{ value: 'WEEK', label: 'Week' },
{ value: 'MONTH', label: 'Month' },
{ value: 'QUARTER', label: 'Quarter' },
{ value: 'YEAR', label: 'Year' },
];
@@ -1,7 +1,7 @@
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { FirstDayOfTheWeek } from 'twenty-shared/types';
import { type RelativeDateFilter } from 'twenty-shared/utils';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
jest.mock('@/localization/utils/detection/detectCalendarStartDay');
@@ -68,6 +68,11 @@ describe('stringifyRelativeDateFilter', () => {
amount: 1,
unit: 'MONTH',
};
const quarterFilter: RelativeDateFilter = {
direction: 'PAST',
amount: 1,
unit: 'QUARTER',
};
const yearFilter: RelativeDateFilter = {
direction: 'PAST',
amount: 1,
@@ -80,6 +85,7 @@ describe('stringifyRelativeDateFilter', () => {
expect(stringifyRelativeDateFilter(dayFilter)).toBe('PAST_1_DAY');
expect(stringifyRelativeDateFilter(weekFilter)).toBe('PAST_1_WEEK');
expect(stringifyRelativeDateFilter(monthFilter)).toBe('PAST_1_MONTH');
expect(stringifyRelativeDateFilter(quarterFilter)).toBe('PAST_1_QUARTER');
expect(stringifyRelativeDateFilter(yearFilter)).toBe('PAST_1_YEAR');
});
});
@@ -4,6 +4,7 @@ import {
endOfHour,
endOfMinute,
endOfMonth,
endOfQuarter,
endOfSecond,
endOfWeek,
endOfYear,
@@ -12,12 +13,14 @@ import {
startOfHour,
startOfMinute,
startOfMonth,
startOfQuarter,
startOfSecond,
startOfWeek,
startOfYear,
} from 'date-fns';
import {
addUnitToDateTime,
assertUnreachable,
getFirstDayOfTheWeekAsANumberForDateFNS,
isDefined,
type RelativeDateFilter,
@@ -169,7 +172,12 @@ function evaluateThisDirection(
start: startOfYear(now),
end: endOfYear(now),
});
case 'QUARTER':
return isWithinInterval(dateToCheck, {
start: startOfQuarter(now),
end: endOfQuarter(now),
});
default:
return false;
return assertUnreachable(unit);
}
}
@@ -1,3 +1,3 @@
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
export type DateTimePeriod = RelativeDateFilterUnit | 'QUARTER';
export type DateTimePeriod = RelativeDateFilterUnit;
@@ -62,6 +62,44 @@ describe('relativeDateFilterStringifiedSchema', () => {
}
});
it('should parse a valid PAST QUARTER filter string', () => {
const result =
relativeDateFilterStringifiedSchema.safeParse('PAST_2_QUARTER');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('PAST');
expect(result.data.amount).toBe(2);
expect(result.data.unit).toBe('QUARTER');
}
});
it('should parse a THIS QUARTER filter string', () => {
const result =
relativeDateFilterStringifiedSchema.safeParse('THIS_1_QUARTER');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('THIS');
expect(result.data.unit).toBe('QUARTER');
}
});
it('should parse a valid NEXT QUARTER filter string', () => {
const result =
relativeDateFilterStringifiedSchema.safeParse('NEXT_1_QUARTER');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('NEXT');
expect(result.data.amount).toBe(1);
expect(result.data.unit).toBe('QUARTER');
}
});
it('should fail for invalid strings', () => {
const result =
relativeDateFilterStringifiedSchema.safeParse('INVALID_STRING');
@@ -67,5 +67,35 @@ describe('resolveRelativeDateFilter', () => {
expect(result.start).toBe('2024-01-01');
expect(result.end).toBe('2025-01-01');
});
it('should compute start and end for THIS QUARTER', () => {
const result = resolveRelativeDateFilter(
{ direction: 'THIS', amount: 1, unit: 'QUARTER' },
referenceZdt,
);
expect(result.start).toBe('2024-01-01');
expect(result.end).toBe('2024-04-01');
});
it('should compute start and end for PAST 1 QUARTER', () => {
const result = resolveRelativeDateFilter(
{ direction: 'PAST', amount: 1, unit: 'QUARTER' },
referenceZdt,
);
expect(result.start).toBe('2023-10-01');
expect(result.end).toBe('2024-01-01');
});
it('should compute start and end for NEXT 1 QUARTER', () => {
const result = resolveRelativeDateFilter(
{ direction: 'NEXT', amount: 1, unit: 'QUARTER' },
referenceZdt,
);
expect(result.start).toBe('2024-04-01');
expect(result.end).toBe('2024-07-01');
});
});
});
@@ -26,6 +26,18 @@ describe('resolveRelativeDateTimeFilter', () => {
expect(result.end?.day).toBe(23);
});
it('should compute for NEXT 1 QUARTER', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: 1, unit: 'QUARTER' },
referenceZdt,
);
expect(result.start?.month).toBe(4);
expect(result.start?.day).toBe(1);
expect(result.end?.month).toBe(7);
expect(result.end?.day).toBe(1);
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateTimeFilter(
@@ -58,6 +70,20 @@ describe('resolveRelativeDateTimeFilter', () => {
expect(result.start?.day).toBe(12);
});
it('should compute for PAST 1 QUARTER', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 1, unit: 'QUARTER' },
referenceZdt,
);
expect(result.start?.month).toBe(10);
expect(result.start?.year).toBe(2023);
expect(result.start?.day).toBe(1);
expect(result.end?.month).toBe(1);
expect(result.end?.year).toBe(2024);
expect(result.end?.day).toBe(1);
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateTimeFilter(
@@ -28,6 +28,8 @@ export const addUnitToDateTime = (
return addWeeks(dateTime, amount);
case 'MONTH':
return addMonths(dateTime, amount);
case 'QUARTER':
return addMonths(dateTime, amount * 3);
case 'YEAR':
return addYears(dateTime, amount);
}
@@ -3,7 +3,7 @@ import { isNonEmptyArray } from '@sniptt/guards';
import z from 'zod';
const REGEX_FOR_RELATIVE_DATE_FILTER_STRINGIFIED_PARSING =
/((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|HOUR|MINUTE|SECOND)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/;
/((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|QUARTER|HOUR|MINUTE|SECOND)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/;
export const relativeDateFilterStringifiedSchema = z
.string()
@@ -7,6 +7,7 @@ export const relativeDateFilterUnitSchema = z.enum([
'DAY',
'WEEK',
'MONTH',
'QUARTER',
'YEAR',
]);
@@ -19,6 +19,35 @@ export const resolveRelativeDateFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceTodayZonedDateTime,
'QUARTER',
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 });
@@ -43,6 +72,29 @@ export const resolveRelativeDateFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceTodayZonedDateTime,
'QUARTER',
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(
@@ -20,6 +20,19 @@ export const resolveRelativeDateTimeFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfNextQuarter = getNextPeriodStart(
referenceZonedDateTime,
'QUARTER',
);
return {
...relativeDateFilter,
start: startOfNextQuarter,
end: addUnitToZonedDateTime(startOfNextQuarter, unit, amount),
};
}
if (isSubDayUnit) {
return {
...relativeDateFilter,
@@ -43,6 +56,19 @@ export const resolveRelativeDateTimeFilter = (
throw new Error('Amount is required');
}
if (unit === 'QUARTER') {
const startOfCurrentQuarter = getPeriodStart(
referenceZonedDateTime,
'QUARTER',
);
return {
...relativeDateFilter,
start: subUnitFromZonedDateTime(startOfCurrentQuarter, unit, amount),
end: startOfCurrentQuarter,
};
}
if (isSubDayUnit) {
return {
...relativeDateFilter,
@@ -27,6 +27,8 @@ export const subUnitFromDateTime = (
return subWeeks(dateTime, amount);
case 'MONTH':
return subMonths(dateTime, amount);
case 'QUARTER':
return subMonths(dateTime, amount * 3);
case 'YEAR':
return subYears(dateTime, amount);
}