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** image --------- 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':