diff --git a/.changeset/tame-paws-clap.md b/.changeset/tame-paws-clap.md new file mode 100644 index 00000000..4159f903 --- /dev/null +++ b/.changeset/tame-paws-clap.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +fix: date/timepicker issue with dates in the future diff --git a/packages/app/src/components/TimePicker/__tests__/utils.test.ts b/packages/app/src/components/TimePicker/__tests__/utils.test.ts index 41a1e81d..6aad6718 100644 --- a/packages/app/src/components/TimePicker/__tests__/utils.test.ts +++ b/packages/app/src/components/TimePicker/__tests__/utils.test.ts @@ -1,10 +1,13 @@ import { dateParser, parseTimeRangeInput } from '../utils'; describe('dateParser', () => { + let mockDate: Date; + beforeEach(() => { // Mock current date to ensure consistent test results jest.useFakeTimers(); - jest.setSystemTime(new Date('2025-01-15T22:00')); + mockDate = new Date('2025-01-15T22:00:00'); + jest.setSystemTime(mockDate); }); afterEach(() => { @@ -44,6 +47,58 @@ describe('dateParser', () => { it('parses non-future month name/date with current year', () => { expect(dateParser('Jan 15')).toEqual(new Date('2025-01-15T12:00:00')); }); + + it('clamps slightly future dates to now (within 1 day) - no year specified', () => { + // Input: 23:00. Now: 22:00. Should clamp to now (22:00) + const result = dateParser('Jan 15 23:00:00'); + expect(result?.getTime()).toEqual(mockDate.getTime()); + expect(result?.getFullYear()).toEqual(2025); + }); + + it('clamps slightly future dates to now (within 1 day) - year specified', () => { + // Explicit year should be preserved even if in future, but clamped to now + const result = dateParser('2025-01-15 23:00:00'); + expect(result?.getTime()).toEqual(mockDate.getTime()); + // Verify it didn't shift to 2024 + expect(result?.getFullYear()).toEqual(2025); + }); + + it('shifts year back for dates more than 1 day in future with inferred year', () => { + // mocked time is 2025-01-15 22:00 + // Jan 17 is more than 1 day in future, should shift to 2024 + const result = dateParser('Jan 17 12:00:00'); + expect(result).toEqual(new Date('2024-01-17T12:00:00')); + }); + + it('does NOT shift year back for dates more than 1 day in future with explicit year', () => { + // mocked time is 2025-01-15 22:00 + // Jan 17, 2025 is more than 1 day in future, but year is explicit + const result = dateParser('2025-01-17 12:00:00'); + expect(result).toEqual(new Date('2025-01-17T12:00:00')); + expect(result?.getFullYear()).toEqual(2025); + }); + + it('handles dates in the past correctly', () => { + // mocked time is 2025-01-15 22:00 + const result = dateParser('Jan 10 12:00:00'); + expect(result).toEqual(new Date('2025-01-10T12:00:00')); + expect(result?.getFullYear()).toEqual(2025); + }); + + it('handles edge case: exactly 1 day in the future', () => { + // mocked time is 2025-01-15 22:00:00 + // Exactly 24 hours later: 2025-01-16 22:00:00 + // Should be clamped since it's <= 1 day from now + const result = dateParser('Jan 16 22:00:00'); + expect(result?.getTime()).toEqual(mockDate.getTime()); + }); + + it('handles edge case: just over 1 day in the future', () => { + // mocked time is 2025-01-15 22:00:00 + // 24 hours + 1 second later should shift year + const result = dateParser('Jan 16 22:00:01'); + expect(result).toEqual(new Date('2024-01-16T22:00:01')); + }); }); describe('parseTimeRangeInput', () => { diff --git a/packages/app/src/components/TimePicker/utils.ts b/packages/app/src/components/TimePicker/utils.ts index 35b7d09a..e1a993e8 100644 --- a/packages/app/src/components/TimePicker/utils.ts +++ b/packages/app/src/components/TimePicker/utils.ts @@ -6,11 +6,12 @@ function normalizeParsedDate(parsed?: chrono.ParsedComponents): Date | null { return null; } - if (parsed.isCertain('year')) { - return parsed.date(); - } - const now = new Date(); + const parsedDate = parsed.date(); + + // If all of the time components have been inferred, set the time components of now + // to match the parsed time components. This ensures that the comparison later on uses + // the same point in time when only worrying about dates. if ( !( parsed.isCertain('hour') || @@ -19,19 +20,27 @@ function normalizeParsedDate(parsed?: chrono.ParsedComponents): Date | null { parsed.isCertain('millisecond') ) ) { - // If all of the time components have been inferred, set the time components of now - // to match the parsed time components. This ensures that the comparison later on uses - // the same point in time when only worrying about dates. now.setHours(parsed.get('hour') || 0); now.setMinutes(parsed.get('minute') || 0); now.setSeconds(parsed.get('second') || 0); now.setMilliseconds(parsed.get('millisecond') || 0); } - const parsedDate = parsed.date(); + // Handle future dates: + // - If slightly in the future (within 1 day), clamp to now + // - If significantly in the future (>1 day) AND year was inferred, shift year back + // (e.g., "Dec 25" typed in June defaults to Dec 25 this year, but user likely meant last Dec 25) if (parsedDate > now) { - parsedDate.setFullYear(parsedDate.getFullYear() - 1); + const oneDayFromNow = now.getTime() + ms('1d'); + if (parsedDate.getTime() <= oneDayFromNow) { + // Slightly in the future: clamp to now + return now; + } else if (!parsed.isCertain('year')) { + // Significantly in the future with inferred year: shift year back + parsedDate.setFullYear(parsedDate.getFullYear() - 1); + } } + return parsedDate; }