From a560e8ac83d9823b939ece5f4f49bfe6337e5052 Mon Sep 17 00:00:00 2001
From: Bhoomaahamso <69999597+Bhoomaahamso@users.noreply.github.com>
Date: Wed, 17 Dec 2025 22:16:11 +0530
Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20added=20higher=20resoulu?=
=?UTF-8?q?tion=20options=20in=20the=20dateTime=20Filter=20(#16548)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Title: "feat: Add second, minute & hour resolution options to relative
date Filter action"
---
## Summary
This PR enables support for smaller time units — **Seconds, Minutes, and
Hours** — in the *Relative Date* filter used in workflows, rather than
being limited to days only.
---
## What Changed
This PR extends the relative date filter to include support for the
following units:
✔️ `SECOND`
✔️ `MINUTE`
✔️ `HOUR`
✔️ (Existing: `DAY`, `WEEK`, `MONTH`, etc.)
Changes include:
- Adding `SECOND`, `MINUTE`, and `HOUR` options to the internal relative
date unit enum/constant.
- Updating utility functions and parsers to correctly interpret and
evaluate these new units.
- Enhancing existing tests and adding new tests to cover second, minute,
and hour relative filters.
---
## Testing
New and updated tests include:
- Unit tests for serialization of relative filter values including
seconds, minutes, and hours.
- Workflow filter evaluation tests that verify minute/hour resolution
behaves correctly.
Tests are included in the changeset.
---
## Backward Compatibility
This change is fully backward compatible:
- All existing relative date filters using days or larger units behave
exactly as before.
- Adding finer units does not alter existing stored data or workflow
definitions.
---
## Issue Reference
Fixes: **twentyhq/twenty#15525**
---------
Co-authored-by: Guillim
Co-authored-by: guillim
---
.../components/FormRelativeDatePicker.tsx | 3 +
.../date/components/DateTimePicker.tsx | 1 +
.../components/RelativeDatePickerHeader.tsx | 8 +-
.../RelativeDateTimeUnitSelectOptions.ts | 15 +
.../stringifyRelativeDateFilter.test.ts | 18 +
.../WorkflowStepFilterValueInput.tsx | 3 +
...evaluate-relative-date-filter.util.spec.ts | 346 ++++++++++++++++++
...-and-evaluate-relative-date-filter.util.ts | 21 ++
.../safeParseRelativeDateFilterValue.test.ts | 138 +++++++
.../filter/dates/utils/addUnitToDateTime.ts | 16 +-
.../dates/utils/getEndUnitOfDateTime.ts | 16 +-
.../dates/utils/getStartUnitOfDateTime.ts | 16 +-
.../relativeDateFilterStringifiedSchema.ts | 2 +-
.../utils/relativeDateFilterUnitSchema.ts | 3 +
.../filter/dates/utils/subUnitFromDateTime.ts | 16 +-
15 files changed, 616 insertions(+), 6 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions.ts
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelativeDatePicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelativeDatePicker.tsx
index 903979c341a..d6b7fbd7f6b 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelativeDatePicker.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormRelativeDatePicker.tsx
@@ -15,12 +15,14 @@ export type FormRelativeDatePickerProps = {
defaultValue?: string;
onChange: (value: RelativeDateFilter) => void;
readonly?: boolean;
+ isDateTimeField?: boolean;
};
export const FormRelativeDatePicker = ({
defaultValue,
onChange,
readonly,
+ isDateTimeField,
}: FormRelativeDatePickerProps) => {
const instanceId = useId();
@@ -48,6 +50,7 @@ export const FormRelativeDatePicker = ({
isFormField={true}
readonly={readonly}
unitDropdownWidth={150}
+ allowIntraDayUnits={isDateTimeField}
/>
);
};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
index e030b16554b..d3a43b0ed65 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
@@ -491,6 +491,7 @@ export const DateTimePicker = ({
amount={relativeDate?.amount}
unit={relativeDate?.unit ?? 'DAY'}
onChange={onRelativeDateChange}
+ allowIntraDayUnits={true}
/>
) : (
{
const amountString = amount?.toString() ?? '';
@@ -50,7 +53,10 @@ export const RelativeDatePickerHeader = ({
const [draftAmountValue, setDraftAmountValue] = useState(amountTextValue);
const isUnitPlural = amount && amount > 1 && direction !== 'THIS';
- const unitSelectOptions = RELATIVE_DATE_UNITS_SELECT_OPTIONS.map((unit) => ({
+ const unitOptionsSource = allowIntraDayUnits
+ ? RELATIVE_DATETIME_UNITS_SELECT_OPTIONS
+ : RELATIVE_DATE_UNITS_SELECT_OPTIONS;
+ const unitSelectOptions = unitOptionsSource.map((unit) => ({
...unit,
label: `${unit.label}${isUnitPlural ? 's' : ''}`,
}));
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions.ts
new file mode 100644
index 00000000000..d5cedd66944
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions.ts
@@ -0,0 +1,15 @@
+import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
+import { type RelativeDateFilterUnit } from 'twenty-shared/utils';
+
+type RelativeDateUnitOption = {
+ value: RelativeDateFilterUnit;
+ label: string;
+};
+
+export const RELATIVE_DATETIME_UNITS_SELECT_OPTIONS: RelativeDateUnitOption[] =
+ [
+ ...RELATIVE_DATE_UNITS_SELECT_OPTIONS,
+ { value: 'HOUR', label: 'Hour' },
+ { value: 'MINUTE', label: 'Minute' },
+ { value: 'SECOND', label: 'Second' },
+ ];
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 c5d28a57266..26e5670a86b 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
@@ -38,6 +38,21 @@ describe('stringifyRelativeDateFilter', () => {
});
it('should stringify with different units', () => {
+ const secondFilter: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 1,
+ unit: 'SECOND',
+ };
+ const minuteFilter: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 1,
+ unit: 'MINUTE',
+ };
+ const hourFilter: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 1,
+ unit: 'HOUR',
+ };
const dayFilter: RelativeDateFilter = {
direction: 'PAST',
amount: 1,
@@ -59,6 +74,9 @@ describe('stringifyRelativeDateFilter', () => {
unit: 'YEAR',
};
+ expect(stringifyRelativeDateFilter(secondFilter)).toBe('PAST_1_SECOND');
+ expect(stringifyRelativeDateFilter(minuteFilter)).toBe('PAST_1_MINUTE');
+ expect(stringifyRelativeDateFilter(hourFilter)).toBe('PAST_1_HOUR');
expect(stringifyRelativeDateFilter(dayFilter)).toBe('PAST_1_DAY');
expect(stringifyRelativeDateFilter(weekFilter)).toBe('PAST_1_WEEK');
expect(stringifyRelativeDateFilter(monthFilter)).toBe('PAST_1_MONTH');
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx
index 351ae5e10c7..d3e867325cf 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx
@@ -134,6 +134,8 @@ export const WorkflowStepFilterValueInput = ({
variableType === FieldMetadataType.DATE_TIME ||
variableType === FieldMetadataType.DATE;
+ const isDateTimeField = variableType === FieldMetadataType.DATE_TIME;
+
const isRelativeDateFilter =
isDateField && stepFilter.operand === ViewFilterOperand.IS_RELATIVE;
@@ -201,6 +203,7 @@ export const WorkflowStepFilterValueInput = ({
defaultValue={relativeDateFilterValue}
onChange={handleRelativeDateFilterChange}
readonly={readonly}
+ isDateTimeField={isDateTimeField}
/>
);
}
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-relative-date-filter.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-relative-date-filter.util.spec.ts
index 7a089d06aad..a3108812bed 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-relative-date-filter.util.spec.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-relative-date-filter.util.spec.ts
@@ -1,10 +1,16 @@
import {
addDays,
+ addHours,
+ addMinutes,
addMonths,
+ addSeconds,
addWeeks,
addYears,
subDays,
+ subHours,
+ subMinutes,
subMonths,
+ subSeconds,
subWeeks,
subYears,
} from 'date-fns';
@@ -104,6 +110,133 @@ describe('Relative Date Filter Utils', () => {
describe('evaluateRelativeDateFilter', () => {
describe('NEXT direction', () => {
+ it('should return true for dates within the next N seconds', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'NEXT',
+ amount: 3,
+ unit: 'SECOND',
+ };
+
+ // Dates within the next 3 seconds should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addSeconds(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addSeconds(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addSeconds(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addSeconds(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within the next N minutes', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'NEXT',
+ amount: 3,
+ unit: 'MINUTE',
+ };
+
+ // Dates within the next 3 minutes should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addMinutes(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addMinutes(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addMinutes(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addMinutes(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within the next N hours', () => {
+ ///
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'NEXT',
+ amount: 3,
+ unit: 'HOUR',
+ };
+
+ // Dates within the next 3 hours should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addHours(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addHours(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addHours(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addHours(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
it('should return true for dates within the next N days', () => {
const relativeDateFilterValue: RelativeDateFilter = {
direction: 'NEXT',
@@ -270,6 +403,132 @@ describe('Relative Date Filter Utils', () => {
});
describe('PAST direction', () => {
+ it('should return true for dates within the past N seconds', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 3,
+ unit: 'SECOND',
+ };
+
+ // Dates within the past 3 seconds should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addSeconds(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within the past N minutes', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 3,
+ unit: 'MINUTE',
+ };
+
+ // Dates within the past 3 minutes should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addMinutes(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within the past N hours', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'PAST',
+ amount: 3,
+ unit: 'HOUR',
+ };
+
+ // Dates within the past 3 hours should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 2),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 3),
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Dates outside the range should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 4),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: addHours(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
it('should return true for dates within the past N days', () => {
const relativeDateFilterValue: RelativeDateFilter = {
direction: 'PAST',
@@ -436,6 +695,93 @@ describe('Relative Date Filter Utils', () => {
});
describe('THIS direction', () => {
+ it('should return true for dates within this second', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'THIS',
+ unit: 'SECOND',
+ };
+
+ // Same second should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: now,
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Different seconds should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: new Date('2024-01-15T08:00:00Z'),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subSeconds(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within this minute', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'THIS',
+ unit: 'MINUTE',
+ };
+
+ // Same minute should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: now,
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Different minutes should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: new Date('2024-01-15T08:00:00Z'),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subMinutes(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
+ it('should return true for dates within this hour', () => {
+ const relativeDateFilterValue: RelativeDateFilter = {
+ direction: 'THIS',
+ unit: 'HOUR',
+ };
+
+ // Same hour should match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: now,
+ relativeDateFilterValue,
+ }),
+ ).toBe(true);
+
+ // Different hours should not match
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: new Date('2024-01-15T08:00:00Z'),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ expect(
+ evaluateRelativeDateFilter({
+ dateToCheck: subHours(now, 1),
+ relativeDateFilterValue,
+ }),
+ ).toBe(false);
+ });
+
it('should return true for dates within this day', () => {
const relativeDateFilterValue: RelativeDateFilter = {
direction: 'THIS',
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 6cec217e1f2..ae89305bf67 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
@@ -1,12 +1,18 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
endOfDay,
+ endOfHour,
+ endOfSecond,
+ endOfMinute,
endOfMonth,
endOfWeek,
endOfYear,
isWithinInterval,
startOfDay,
+ startOfHour,
+ startOfMinute,
startOfMonth,
+ startOfSecond,
startOfWeek,
startOfYear,
} from 'date-fns';
@@ -122,6 +128,21 @@ function evaluateThisDirection(
: 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),
diff --git a/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts b/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts
index b46102d3f12..2b425e2111b 100644
--- a/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts
+++ b/packages/twenty-shared/src/utils/__tests__/safeParseRelativeDateFilterValue.test.ts
@@ -3,6 +3,54 @@ import { safeParseRelativeDateFilterJSONStringified } from '@/utils/safeParseRel
describe('safeParseRelativeDateFilterJSONStringified', () => {
describe('valid inputs', () => {
describe('NEXT direction', () => {
+ it('should parse NEXT direction with SECOND unit', () => {
+ const input = JSON.stringify({
+ direction: 'NEXT',
+ amount: 30,
+ unit: 'SECOND',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'NEXT',
+ amount: 30,
+ unit: 'SECOND',
+ });
+ });
+
+ it('should parse NEXT direction with MINUTE unit', () => {
+ const input = JSON.stringify({
+ direction: 'NEXT',
+ amount: 15,
+ unit: 'MINUTE',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'NEXT',
+ amount: 15,
+ unit: 'MINUTE',
+ });
+ });
+
+ it('should parse NEXT direction with HOUR unit', () => {
+ const input = JSON.stringify({
+ direction: 'NEXT',
+ amount: 6,
+ unit: 'HOUR',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'NEXT',
+ amount: 6,
+ unit: 'HOUR',
+ });
+ });
+
it('should parse NEXT direction with DAY unit', () => {
const input = JSON.stringify({
direction: 'NEXT',
@@ -69,6 +117,54 @@ describe('safeParseRelativeDateFilterJSONStringified', () => {
});
describe('PAST direction', () => {
+ it('should parse PAST direction with SECOND unit', () => {
+ const input = JSON.stringify({
+ direction: 'PAST',
+ amount: 45,
+ unit: 'SECOND',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'PAST',
+ amount: 45,
+ unit: 'SECOND',
+ });
+ });
+
+ it('should parse PAST direction with MINUTE unit', () => {
+ const input = JSON.stringify({
+ direction: 'PAST',
+ amount: 20,
+ unit: 'MINUTE',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'PAST',
+ amount: 20,
+ unit: 'MINUTE',
+ });
+ });
+
+ it('should parse PAST direction with HOUR unit', () => {
+ const input = JSON.stringify({
+ direction: 'PAST',
+ amount: 5,
+ unit: 'HOUR',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'PAST',
+ amount: 5,
+ unit: 'HOUR',
+ });
+ });
+
it('should parse PAST direction with DAY unit', () => {
const input = JSON.stringify({
direction: 'PAST',
@@ -135,6 +231,48 @@ describe('safeParseRelativeDateFilterJSONStringified', () => {
});
describe('THIS direction', () => {
+ it('should parse THIS direction with SECOND unit (no amount)', () => {
+ const input = JSON.stringify({
+ direction: 'THIS',
+ unit: 'SECOND',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'THIS',
+ unit: 'SECOND',
+ });
+ });
+
+ it('should parse THIS direction with MINUTE unit (no amount)', () => {
+ const input = JSON.stringify({
+ direction: 'THIS',
+ unit: 'MINUTE',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'THIS',
+ unit: 'MINUTE',
+ });
+ });
+
+ it('should parse THIS direction with HOUR unit (no amount)', () => {
+ const input = JSON.stringify({
+ direction: 'THIS',
+ unit: 'HOUR',
+ });
+
+ const result = safeParseRelativeDateFilterJSONStringified(input);
+
+ expect(result).toEqual({
+ direction: 'THIS',
+ unit: 'HOUR',
+ });
+ });
+
it('should parse THIS direction with DAY unit (no amount)', () => {
const input = JSON.stringify({
direction: 'THIS',
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 e61b8102a4a..8e5a5b3db05 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/addUnitToDateTime.ts
@@ -1,5 +1,13 @@
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
-import { addDays, addMonths, addWeeks, addYears } from 'date-fns';
+import {
+ addDays,
+ addHours,
+ addMinutes,
+ addMonths,
+ addSeconds,
+ addWeeks,
+ addYears,
+} from 'date-fns';
export const addUnitToDateTime = (
dateTime: Date,
@@ -7,6 +15,12 @@ export const addUnitToDateTime = (
unit: RelativeDateFilterUnit,
) => {
switch (unit) {
+ case 'SECOND':
+ return addSeconds(dateTime, amount);
+ case 'MINUTE':
+ return addMinutes(dateTime, amount);
+ case 'HOUR':
+ return addHours(dateTime, amount);
case 'DAY':
return addDays(dateTime, amount);
case 'WEEK':
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/getEndUnitOfDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/getEndUnitOfDateTime.ts
index e95fd65a1f8..cff850187b9 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/getEndUnitOfDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/getEndUnitOfDateTime.ts
@@ -3,7 +3,15 @@ import { type FirstDayOfTheWeek } from '@/utils/filter/dates/utils/firstDayOfWee
import { getFirstDayOfTheWeekAsANumberForDateFNS } from '@/utils/filter/dates/utils/getFirstDayOfTheWeekAsANumberForDateFNS';
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
import { isDefined } from '@/utils/validation';
-import { endOfDay, endOfMonth, endOfWeek, endOfYear } from 'date-fns';
+import {
+ endOfDay,
+ endOfHour,
+ endOfMinute,
+ endOfMonth,
+ endOfSecond,
+ endOfWeek,
+ endOfYear,
+} from 'date-fns';
export const getEndUnitOfDateTime = (
dateTime: Date,
@@ -11,6 +19,12 @@ export const getEndUnitOfDateTime = (
firstDayOfTheWeek?: Nullable,
) => {
switch (unit) {
+ case 'SECOND':
+ return endOfSecond(dateTime);
+ case 'MINUTE':
+ return endOfMinute(dateTime);
+ case 'HOUR':
+ return endOfHour(dateTime);
case 'DAY':
return endOfDay(dateTime);
case 'WEEK': {
diff --git a/packages/twenty-shared/src/utils/filter/dates/utils/getStartUnitOfDateTime.ts b/packages/twenty-shared/src/utils/filter/dates/utils/getStartUnitOfDateTime.ts
index eb32b0f0ff2..6017e48d125 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/getStartUnitOfDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/getStartUnitOfDateTime.ts
@@ -3,7 +3,15 @@ import { type FirstDayOfTheWeek } from '@/utils/filter/dates/utils/firstDayOfWee
import { getFirstDayOfTheWeekAsANumberForDateFNS } from '@/utils/filter/dates/utils/getFirstDayOfTheWeekAsANumberForDateFNS';
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
import { isDefined } from '@/utils/validation';
-import { startOfDay, startOfMonth, startOfWeek, startOfYear } from 'date-fns';
+import {
+ startOfDay,
+ startOfHour,
+ startOfMinute,
+ startOfMonth,
+ startOfSecond,
+ startOfWeek,
+ startOfYear,
+} from 'date-fns';
export const getStartUnitOfDateTime = (
dateTime: Date,
@@ -11,6 +19,12 @@ export const getStartUnitOfDateTime = (
firstDayOfTheWeek?: Nullable,
) => {
switch (unit) {
+ case 'SECOND':
+ return startOfSecond(dateTime);
+ case 'MINUTE':
+ return startOfMinute(dateTime);
+ case 'HOUR':
+ return startOfHour(dateTime);
case 'DAY':
return startOfDay(dateTime);
case 'WEEK': {
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 1bc8caf5507..7af14aebf8f 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)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/;
+ /((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|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 14724bde848..09b35942527 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/relativeDateFilterUnitSchema.ts
@@ -1,6 +1,9 @@
import z from 'zod';
export const relativeDateFilterUnitSchema = z.enum([
+ 'SECOND',
+ 'MINUTE',
+ 'HOUR',
'DAY',
'WEEK',
'MONTH',
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 7a7179fb058..95aa9a10e75 100644
--- a/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts
+++ b/packages/twenty-shared/src/utils/filter/dates/utils/subUnitFromDateTime.ts
@@ -1,5 +1,13 @@
import { type RelativeDateFilterUnit } from '@/utils/filter/dates/utils/relativeDateFilterUnitSchema';
-import { subDays, subMonths, subWeeks, subYears } from 'date-fns';
+import {
+ subDays,
+ subHours,
+ subMinutes,
+ subMonths,
+ subSeconds,
+ subWeeks,
+ subYears,
+} from 'date-fns';
export const subUnitFromDateTime = (
dateTime: Date,
@@ -7,6 +15,12 @@ export const subUnitFromDateTime = (
unit: RelativeDateFilterUnit,
) => {
switch (unit) {
+ case 'SECOND':
+ return subSeconds(dateTime, amount);
+ case 'MINUTE':
+ return subMinutes(dateTime, amount);
+ case 'HOUR':
+ return subHours(dateTime, amount);
case 'DAY':
return subDays(dateTime, amount);
case 'WEEK':