Skip to content

Commit adb47a1

Browse files
committed
fix(welcome): handle future workspace access dates
- Compare recent workspace timestamps by local calendar date - Show localized absolute dates for future calendar days - Cover same-day, midnight, DST, and future-date boundaries
1 parent c9281f0 commit adb47a1

3 files changed

Lines changed: 92 additions & 10 deletions

File tree

src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createLogger } from '@/shared/utils/logger';
1818
import type { SceneTabId } from '@/app/components/SceneBar/types';
1919
import type { WorkspaceInfo } from '@/shared/types';
2020
import { getRecentWorkspaceLineParts } from '@/shared/utils/recentWorkspaceDisplay';
21+
import { formatRecentWorkspaceDate } from './recentWorkspaceDate';
2122
import './WelcomeScene.scss';
2223

2324
const log = createLogger('WelcomeScene');
@@ -95,16 +96,7 @@ const WelcomeScene: React.FC = () => {
9596

9697
const formatDate = useCallback((dateString: string) => {
9798
try {
98-
const date = new Date(dateString);
99-
const now = new Date();
100-
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
101-
const dateStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
102-
const diffDays = Math.round((todayStart - dateStart) / (1000 * 60 * 60 * 24));
103-
if (diffDays <= 0) return t('time.today');
104-
if (diffDays === 1) return t('time.yesterday');
105-
if (diffDays < 7) return t('startup.daysAgo', { count: diffDays });
106-
if (diffDays < 30) return t('startup.weeksAgo', { count: Math.ceil(diffDays / 7) });
107-
return formatLocaleDate(date);
99+
return formatRecentWorkspaceDate(dateString, new Date(), t, formatLocaleDate);
108100
} catch {
109101
return '';
110102
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import {
3+
differenceInLocalCalendarDays,
4+
formatRecentWorkspaceDate,
5+
} from './recentWorkspaceDate';
6+
7+
const t = vi.fn((key: string, options?: Record<string, unknown>) => (
8+
options?.count === undefined ? key : `${key}:${options.count}`
9+
));
10+
const formatLocaleDate = vi.fn((date: Date) => `absolute:${date.toISOString()}`);
11+
12+
describe('formatRecentWorkspaceDate', () => {
13+
it('labels an earlier time on the same calendar day as today', () => {
14+
const date = new Date(2026, 7, 6, 0, 1);
15+
16+
expect(formatRecentWorkspaceDate(
17+
date.toISOString(),
18+
new Date(2026, 7, 6, 23, 59),
19+
t,
20+
formatLocaleDate,
21+
)).toBe('time.today');
22+
});
23+
24+
it('labels the previous calendar day as yesterday even when less than 24 hours ago', () => {
25+
const date = new Date(2026, 7, 5, 23, 59);
26+
27+
expect(formatRecentWorkspaceDate(
28+
date.toISOString(),
29+
new Date(2026, 7, 6, 0, 1),
30+
t,
31+
formatLocaleDate,
32+
)).toBe('time.yesterday');
33+
});
34+
35+
it.each([
36+
['spring-forward', 23, new Date('2026-03-09T04:30:00Z'), new Date('2026-03-08T05:30:00Z')],
37+
['fall-back', 25, new Date('2026-11-02T05:30:00Z'), new Date('2026-11-01T04:30:00Z')],
38+
])('uses calendar dates across a %s boundary', (_name, elapsedHours, now, date) => {
39+
vi.spyOn(now, 'getFullYear').mockReturnValue(2026);
40+
vi.spyOn(now, 'getMonth').mockReturnValue(_name === 'spring-forward' ? 2 : 10);
41+
vi.spyOn(now, 'getDate').mockReturnValue(_name === 'spring-forward' ? 9 : 2);
42+
vi.spyOn(date, 'getFullYear').mockReturnValue(2026);
43+
vi.spyOn(date, 'getMonth').mockReturnValue(_name === 'spring-forward' ? 2 : 10);
44+
vi.spyOn(date, 'getDate').mockReturnValue(_name === 'spring-forward' ? 8 : 1);
45+
46+
expect((now.getTime() - date.getTime()) / (60 * 60 * 1000)).toBe(elapsedHours);
47+
expect(differenceInLocalCalendarDays(now, date)).toBe(1);
48+
});
49+
50+
it('formats a future calendar date as an absolute date', () => {
51+
const date = new Date(2026, 7, 7, 0, 20);
52+
53+
expect(formatRecentWorkspaceDate(
54+
date.toISOString(),
55+
new Date(2026, 7, 6, 23, 50),
56+
t,
57+
formatLocaleDate,
58+
)).toBe(`absolute:${date.toISOString()}`);
59+
});
60+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
2+
3+
type Translate = (key: string, options?: Record<string, unknown>) => string;
4+
type FormatLocaleDate = (date: Date) => string;
5+
6+
function localCalendarDayNumber(date: Date): number {
7+
return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / MILLISECONDS_PER_DAY;
8+
}
9+
10+
export function differenceInLocalCalendarDays(now: Date, date: Date): number {
11+
return localCalendarDayNumber(now) - localCalendarDayNumber(date);
12+
}
13+
14+
export function formatRecentWorkspaceDate(
15+
dateString: string,
16+
now: Date,
17+
t: Translate,
18+
formatLocaleDate: FormatLocaleDate,
19+
): string {
20+
const date = new Date(dateString);
21+
const diffDays = differenceInLocalCalendarDays(now, date);
22+
23+
if (diffDays === 0) return t('time.today');
24+
if (diffDays === 1) return t('time.yesterday');
25+
if (diffDays > 1 && diffDays < 7) return t('startup.daysAgo', { count: diffDays });
26+
if (diffDays >= 7 && diffDays < 30) {
27+
return t('startup.weeksAgo', { count: Math.ceil(diffDays / 7) });
28+
}
29+
return formatLocaleDate(date);
30+
}

0 commit comments

Comments
 (0)