mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
fix: align PAST/NEXT relative date filter for WEEK/MONTH/YEAR to use calendar-aligned periods like QUARTER
https://sonarly.com/issue/26866?type=bug The PAST and NEXT relative date filter directions use two different calculation strategies: QUARTER uses calendar-aligned periods (start of quarter boundary), while WEEK, MONTH, and YEAR use a rolling window from the current day. This makes it impossible to filter by "last calendar week" or "last calendar month". Fix: ## What changed Aligned PAST and NEXT relative date filter directions to use calendar-aligned periods for all units (WEEK, MONTH, YEAR, QUARTER), consistent with how THIS direction already worked and how QUARTER was specially handled. ### `resolveRelativeDateFilter.ts` - **NEXT direction**: Replaced the QUARTER-specific special case and the rolling-window fallback with a single unified path using `getNextPeriodStart()` for all units. For example, "Next 1 Week" now returns the start of next calendar week to the end of next calendar week, instead of tomorrow to tomorrow+7 days. - **PAST direction**: Same approach — replaced QUARTER special case and rolling fallback with `getPeriodStart()` for all units. "Past 1 Week" now returns previous calendar week boundaries instead of the last 7 days. - **DAY behavior is preserved**: `getPeriodStart('DAY')` returns `startOfDay()` and `getNextPeriodStart('DAY')` returns `startOfDay() + 1 day`, producing identical results to the old rolling-window code. ### `resolveRelativeDateTimeFilter.ts` - Same fix applied, but preserving the sub-day unit (SECOND, MINUTE, HOUR) rolling behavior since those don't have calendar period boundaries. - Removed QUARTER special case and replaced the non-sub-day fallback with `getNextPeriodStart()`/`getPeriodStart()` calls. ### Tests - Added test cases for PAST 1 WEEK, PAST 1 MONTH, PAST 1 YEAR, NEXT 1 WEEK, NEXT 1 MONTH, NEXT 1 YEAR in both test files to verify calendar-aligned behavior. - Existing DAY and QUARTER tests remain unchanged and continue to pass. ### Not changed - The workflow filter utility (`parse-and-evaluate-relative-date-filter.util.ts`) has the same conceptual bug but uses `date-fns` and already has a TODO to merge with the shared logic. It is not part of this fix to keep scope focused.
This commit is contained in:
parent
cddc47b61f
commit
337685e759
4 changed files with 157 additions and 108 deletions
|
|
@ -16,6 +16,37 @@ describe('resolveRelativeDateFilter', () => {
|
|||
expect(result.end).toBe('2024-03-23');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for NEXT 1 WEEK', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'WEEK' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Mar 15 is Friday, next week starts Monday Mar 18
|
||||
expect(result.start).toBe('2024-03-18');
|
||||
expect(result.end).toBe('2024-03-25');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for NEXT 1 MONTH', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2024-04-01');
|
||||
expect(result.end).toBe('2024-05-01');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for NEXT 1 YEAR', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'YEAR' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2025-01-01');
|
||||
expect(result.end).toBe('2026-01-01');
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateFilter(
|
||||
|
|
@ -37,6 +68,39 @@ describe('resolveRelativeDateFilter', () => {
|
|||
expect(result.end).toBe('2024-03-15');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for PAST 1 WEEK', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'WEEK' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Mar 15 is Friday, current week starts Monday Mar 11, past 1 week = Mar 4-11
|
||||
expect(result.start).toBe('2024-03-04');
|
||||
expect(result.end).toBe('2024-03-11');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for PAST 1 MONTH', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Current month starts Mar 1, past 1 month = Feb 1-Mar 1
|
||||
expect(result.start).toBe('2024-02-01');
|
||||
expect(result.end).toBe('2024-03-01');
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned start and end for PAST 1 YEAR', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'YEAR' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Current year starts Jan 1 2024, past 1 year = Jan 1 2023-Jan 1 2024
|
||||
expect(result.start).toBe('2023-01-01');
|
||||
expect(result.end).toBe('2024-01-01');
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateFilter(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,31 @@ describe('resolveRelativeDateTimeFilter', () => {
|
|||
expect(result.end?.day).toBe(23);
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned for NEXT 1 WEEK', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'WEEK' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Mar 15 is Friday, next week starts Monday Mar 18
|
||||
expect(result.start?.month).toBe(3);
|
||||
expect(result.start?.day).toBe(18);
|
||||
expect(result.end?.month).toBe(3);
|
||||
expect(result.end?.day).toBe(25);
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned for NEXT 1 MONTH', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start?.month).toBe(4);
|
||||
expect(result.start?.day).toBe(1);
|
||||
expect(result.end?.month).toBe(5);
|
||||
expect(result.end?.day).toBe(1);
|
||||
});
|
||||
|
||||
it('should compute for NEXT 1 QUARTER', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: 1, unit: 'QUARTER' },
|
||||
|
|
@ -70,6 +95,32 @@ describe('resolveRelativeDateTimeFilter', () => {
|
|||
expect(result.start?.day).toBe(12);
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned for PAST 1 WEEK', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'WEEK' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Mar 15 is Friday, current week starts Monday Mar 11, past 1 week = Mar 4-11
|
||||
expect(result.start?.month).toBe(3);
|
||||
expect(result.start?.day).toBe(4);
|
||||
expect(result.end?.month).toBe(3);
|
||||
expect(result.end?.day).toBe(11);
|
||||
});
|
||||
|
||||
it('should compute calendar-aligned for PAST 1 MONTH', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
// Current month starts Mar 1, past 1 month = Feb 1-Mar 1
|
||||
expect(result.start?.month).toBe(2);
|
||||
expect(result.start?.day).toBe(1);
|
||||
expect(result.end?.month).toBe(3);
|
||||
expect(result.end?.day).toBe(1);
|
||||
});
|
||||
|
||||
it('should compute for PAST 1 QUARTER', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: 1, unit: 'QUARTER' },
|
||||
|
|
|
|||
|
|
@ -19,47 +19,20 @@ export const resolveRelativeDateFilter = (
|
|||
throw new Error('Amount is required');
|
||||
}
|
||||
|
||||
if (unit === 'QUARTER') {
|
||||
const startOfCurrentQuarter = getPeriodStart(
|
||||
referenceTodayZonedDateTime,
|
||||
'QUARTER',
|
||||
firstDayOfTheWeek,
|
||||
);
|
||||
const startOfNextPeriod = getNextPeriodStart(
|
||||
referenceTodayZonedDateTime,
|
||||
unit,
|
||||
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 });
|
||||
|
||||
const startOfNextPeriod = addUnitToZonedDateTime(
|
||||
startOfNextDay,
|
||||
const endOfNextPeriod = addUnitToZonedDateTime(
|
||||
startOfNextPeriod,
|
||||
unit,
|
||||
amount,
|
||||
);
|
||||
|
||||
const start = startOfNextDay.toPlainDate().toString();
|
||||
const end = startOfNextPeriod?.toPlainDate().toString();
|
||||
const start = startOfNextPeriod.toPlainDate().toString();
|
||||
const end = endOfNextPeriod.toPlainDate().toString();
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
|
|
@ -72,39 +45,20 @@ export const resolveRelativeDateFilter = (
|
|||
throw new Error('Amount is required');
|
||||
}
|
||||
|
||||
if (unit === 'QUARTER') {
|
||||
const startOfCurrentQuarter = getPeriodStart(
|
||||
referenceTodayZonedDateTime,
|
||||
'QUARTER',
|
||||
firstDayOfTheWeek,
|
||||
);
|
||||
const startOfCurrentPeriod = getPeriodStart(
|
||||
referenceTodayZonedDateTime,
|
||||
unit,
|
||||
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(
|
||||
startOfDay,
|
||||
const startOfPastPeriod = subUnitFromZonedDateTime(
|
||||
startOfCurrentPeriod,
|
||||
unit,
|
||||
amount,
|
||||
);
|
||||
|
||||
const start = startOfNextPeriod?.toPlainDate().toString();
|
||||
const end = startOfDay.toPlainDate().toString();
|
||||
const start = startOfPastPeriod.toPlainDate().toString();
|
||||
const end = startOfCurrentPeriod.toPlainDate().toString();
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
|
|
|
|||
|
|
@ -20,70 +20,50 @@ 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,
|
||||
start: referenceZonedDateTime,
|
||||
end: addUnitToZonedDateTime(referenceZonedDateTime, unit, amount),
|
||||
};
|
||||
} else {
|
||||
const startOfNextDay = referenceZonedDateTime
|
||||
.startOfDay()
|
||||
.add({ days: 1 });
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
start: startOfNextDay,
|
||||
end: addUnitToZonedDateTime(startOfNextDay, unit, amount),
|
||||
};
|
||||
}
|
||||
|
||||
const startOfNextPeriod = getNextPeriodStart(
|
||||
referenceZonedDateTime,
|
||||
unit,
|
||||
firstDayOfTheWeek,
|
||||
);
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
start: startOfNextPeriod,
|
||||
end: addUnitToZonedDateTime(startOfNextPeriod, unit, amount),
|
||||
};
|
||||
}
|
||||
case 'PAST': {
|
||||
if (!isDefined(amount)) {
|
||||
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,
|
||||
start: subUnitFromZonedDateTime(referenceZonedDateTime, unit, amount),
|
||||
end: referenceZonedDateTime,
|
||||
};
|
||||
} else {
|
||||
const startOfDay = referenceZonedDateTime.startOfDay();
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
start: subUnitFromZonedDateTime(startOfDay, unit, amount),
|
||||
end: startOfDay,
|
||||
};
|
||||
}
|
||||
|
||||
const startOfCurrentPeriod = getPeriodStart(
|
||||
referenceZonedDateTime,
|
||||
unit,
|
||||
firstDayOfTheWeek,
|
||||
);
|
||||
|
||||
return {
|
||||
...relativeDateFilter,
|
||||
start: subUnitFromZonedDateTime(startOfCurrentPeriod, unit, amount),
|
||||
end: startOfCurrentPeriod,
|
||||
};
|
||||
}
|
||||
case 'THIS':
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue