From 40a8d18d3815defbb474fe97749548f8b09358e7 Mon Sep 17 00:00:00 2001
From: Arun <184187559+carbonFibreCode@users.noreply.github.com>
Date: Tue, 10 Mar 2026 22:12:10 +0530
Subject: [PATCH] feat : Added "Quarter" as a time unity to filter on date
(#18289)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
fixes #16674
Simply added the Quarter functionality to the existing filter code with
months amount multiplied by 3.
---------
Co-authored-by: Arun kumar
Co-authored-by: Lucas Bordeau
---
.../RelativeDateUnitSelectOptions.ts | 1 +
.../stringifyRelativeDateFilter.test.ts | 8 ++-
...-and-evaluate-relative-date-filter.util.ts | 10 +++-
.../filter/dates/types/DateTimePeriod.ts | 2 +-
...elativeDateFilterStringifiedSchema.test.ts | 38 ++++++++++++++
.../resolveRelativeDateFilter.test.ts | 30 +++++++++++
.../resolveRelativeDateTimeFilter.test.ts | 26 ++++++++++
.../filter/dates/utils/addUnitToDateTime.ts | 2 +
.../relativeDateFilterStringifiedSchema.ts | 2 +-
.../utils/relativeDateFilterUnitSchema.ts | 1 +
.../dates/utils/resolveRelativeDateFilter.ts | 52 +++++++++++++++++++
.../utils/resolveRelativeDateTimeFilter.ts | 26 ++++++++++
.../filter/dates/utils/subUnitFromDateTime.ts | 2 +
13 files changed, 196 insertions(+), 4 deletions(-)
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts
index 3072341639..6b89613093 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts
@@ -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' },
];
diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts
index a3d8ff5044..7e4dc916a7 100644
--- a/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts
+++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/__tests__/stringifyRelativeDateFilter.test.ts
@@ -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');
});
});
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts
index 9a530f70d4..69f605ad4e 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util.ts
@@ -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);
}
}
diff --git a/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts b/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts
index 7fc5695523..73dc37cfd5 100644
--- a/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/types/DateTimePeriod.ts
@@ -1,3 +1,3 @@
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
-export type DateTimePeriod = RelativeDateFilterUnit | 'QUARTER';
+export type DateTimePeriod = RelativeDateFilterUnit;
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts
index 49cec63a0d..d9457b2412 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/relativeDateFilterStringifiedSchema.test.ts
@@ -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');
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts
index a08c0697e6..84fc9aa46e 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateFilter.test.ts
@@ -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');
+ });
});
});
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts
index 181040cae3..029f22ee4a 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/__tests__/resolveRelativeDateTimeFilter.test.ts
@@ -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(
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts
index 04261b4f04..def3626d90 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts
@@ -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);
}
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts
index fc9dbbca35..91b0c78b9c 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterStringifiedSchema.ts
@@ -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()
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts
index 09b3594252..dc61a6ae71 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts
@@ -7,6 +7,7 @@ export const relativeDateFilterUnitSchema = z.enum([
'DAY',
'WEEK',
'MONTH',
+ 'QUARTER',
'YEAR',
]);
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts
index 5bab58f4c6..b228322703 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateFilter.ts
@@ -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(
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts
index bdd563a056..6fb9748082 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/resolveRelativeDateTimeFilter.ts
@@ -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,
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts
index 95aa9a10e7..820b26fce8 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts
@@ -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);
}