feat: 🎸 added higher resoulution options in the dateTime Filter (#16548)
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** <img width="1909" height="896" alt="image" src="https://github.com/user-attachments/assets/328d03dc-ca0b-4c3f-84e5-58961c178398" /> --------- Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
+3
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -491,6 +491,7 @@ export const DateTimePicker = ({
|
||||
amount={relativeDate?.amount}
|
||||
unit={relativeDate?.unit ?? 'DAY'}
|
||||
onChange={onRelativeDateChange}
|
||||
allowIntraDayUnits={true}
|
||||
/>
|
||||
) : (
|
||||
<DateTimePickerHeader
|
||||
|
||||
+7
-1
@@ -1,6 +1,7 @@
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions';
|
||||
import { RELATIVE_DATETIME_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions';
|
||||
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
@@ -30,6 +31,7 @@ type RelativeDatePickerHeaderProps = {
|
||||
isFormField?: boolean;
|
||||
readonly?: boolean;
|
||||
unitDropdownWidth?: number;
|
||||
allowIntraDayUnits?: boolean;
|
||||
};
|
||||
|
||||
export const RelativeDatePickerHeader = ({
|
||||
@@ -41,6 +43,7 @@ export const RelativeDatePickerHeader = ({
|
||||
onChange,
|
||||
readonly,
|
||||
unitDropdownWidth,
|
||||
allowIntraDayUnits,
|
||||
}: RelativeDatePickerHeaderProps) => {
|
||||
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' : ''}`,
|
||||
}));
|
||||
|
||||
+15
@@ -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' },
|
||||
];
|
||||
+18
@@ -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');
|
||||
|
||||
+3
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+346
@@ -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',
|
||||
|
||||
+21
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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<FirstDayOfTheWeek>,
|
||||
) => {
|
||||
switch (unit) {
|
||||
case 'SECOND':
|
||||
return endOfSecond(dateTime);
|
||||
case 'MINUTE':
|
||||
return endOfMinute(dateTime);
|
||||
case 'HOUR':
|
||||
return endOfHour(dateTime);
|
||||
case 'DAY':
|
||||
return endOfDay(dateTime);
|
||||
case 'WEEK': {
|
||||
|
||||
@@ -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<FirstDayOfTheWeek>,
|
||||
) => {
|
||||
switch (unit) {
|
||||
case 'SECOND':
|
||||
return startOfSecond(dateTime);
|
||||
case 'MINUTE':
|
||||
return startOfMinute(dateTime);
|
||||
case 'HOUR':
|
||||
return startOfHour(dateTime);
|
||||
case 'DAY':
|
||||
return startOfDay(dateTime);
|
||||
case 'WEEK': {
|
||||
|
||||
+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)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/;
|
||||
/((?:THIS)|(?:PAST)|(?:NEXT))_(\d*)_(DAY|MONTH|YEAR|WEEK|HOUR|MINUTE|SECOND)(?:(?:;;([^;;]*);;)?(?:(MONDAY|SUNDAY|SATURDAY);;)?)?/;
|
||||
|
||||
export const relativeDateFilterStringifiedSchema = z
|
||||
.string()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const relativeDateFilterUnitSchema = z.enum([
|
||||
'SECOND',
|
||||
'MINUTE',
|
||||
'HOUR',
|
||||
'DAY',
|
||||
'WEEK',
|
||||
'MONTH',
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user