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:
+1
@@ -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' },
|
||||
];
|
||||
|
||||
+7
-1
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+9
-1
@@ -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;
|
||||
|
||||
+38
@@ -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');
|
||||
|
||||
+30
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user