From d0dc52f92584e6d364f9ee6d68af96689c224b98 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 16 Jun 2026 01:44:54 +0200 Subject: [PATCH] (feat:dateTime) consistent d/m/y --- frontend/src/admin/AdminUI.tsx | 15 +-- frontend/src/agents/AgentLogs.tsx | 3 +- frontend/src/agents/SharedAgentCard.tsx | 14 +- .../src/agents/schedules/RunDetailDrawer.tsx | 6 +- frontend/src/agents/schedules/RunLog.tsx | 6 +- .../agents/schedules/ScheduleFormModal.tsx | 9 +- .../schedules/SchedulerToolCallCard.tsx | 6 +- .../src/agents/schedules/SchedulesView.tsx | 8 +- frontend/src/components/FilePicker.tsx | 4 +- .../src/conversation/SharedConversation.tsx | 4 +- frontend/src/settings/RemoteDeviceConfig.tsx | 8 +- frontend/src/utils/dateTimeUtils.test.ts | 43 ++++++ frontend/src/utils/dateTimeUtils.ts | 125 ++++++++++-------- 13 files changed, 135 insertions(+), 116 deletions(-) create mode 100644 frontend/src/utils/dateTimeUtils.test.ts diff --git a/frontend/src/admin/AdminUI.tsx b/frontend/src/admin/AdminUI.tsx index c83055f5..e62ab82b 100644 --- a/frontend/src/admin/AdminUI.tsx +++ b/frontend/src/admin/AdminUI.tsx @@ -1,4 +1,5 @@ import Spinner from '../components/Spinner'; +import { formatDateOnly, formatDateTime } from '../utils/dateTimeUtils'; export function Loading() { return ( @@ -68,21 +69,11 @@ export function Pill({ } export function fmtDate(value?: string | null): string { - if (!value) return '—'; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString(); + return value ? formatDateTime(value) : '—'; } export function fmtDateShort(value?: string | null): string { - if (!value) return '—'; - const date = new Date(value); - return Number.isNaN(date.getTime()) - ? String(value) - : date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - }); + return value ? formatDateOnly(value) : '—'; } export function fmtRelative(value?: string | null): string { diff --git a/frontend/src/agents/AgentLogs.tsx b/frontend/src/agents/AgentLogs.tsx index ec9fc53e..59994701 100644 --- a/frontend/src/agents/AgentLogs.tsx +++ b/frontend/src/agents/AgentLogs.tsx @@ -8,6 +8,7 @@ import Spinner from '../components/Spinner'; import { selectToken } from '../preferences/preferenceSlice'; import Analytics from '../settings/Analytics'; import Logs from '../settings/Logs'; +import { formatDateTime } from '../utils/dateTimeUtils'; import AgentPageHeader from './AgentPageHeader'; import { Agent } from './types'; @@ -59,7 +60,7 @@ export default function AgentLogs() { {agent.last_used_at ? t('agents.logs.lastUsedAt') + ' ' + - new Date(agent.last_used_at).toLocaleString() + formatDateTime(agent.last_used_at) : t('agents.logs.noUsageHistory')}

diff --git a/frontend/src/agents/SharedAgentCard.tsx b/frontend/src/agents/SharedAgentCard.tsx index f7745fe0..d5fb8e42 100644 --- a/frontend/src/agents/SharedAgentCard.tsx +++ b/frontend/src/agents/SharedAgentCard.tsx @@ -4,6 +4,7 @@ import EditIcon from '../assets/edit.svg'; import ToolIcon from '../components/ToolIcon'; import { Avatar } from '../components/ui/avatar'; import { Button } from '../components/ui/button'; +import { formatDateTime } from '../utils/dateTimeUtils'; import { getToolDisplayName } from '../utils/toolUtils'; import { Agent } from './types'; @@ -61,18 +62,7 @@ export default function SharedAgentCard({ )} {agent.shared_metadata?.shared_at && (

- Shared on{' '} - {new Date(agent.shared_metadata.shared_at).toLocaleString( - 'en-US', - { - month: 'long', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: true, - }, - )} + Shared on {formatDateTime(agent.shared_metadata.shared_at)}

)} diff --git a/frontend/src/agents/schedules/RunDetailDrawer.tsx b/frontend/src/agents/schedules/RunDetailDrawer.tsx index 7cd85876..4c1359bd 100644 --- a/frontend/src/agents/schedules/RunDetailDrawer.tsx +++ b/frontend/src/agents/schedules/RunDetailDrawer.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { Button } from '../../components/ui/button'; +import { formatDateTime } from '../../utils/dateTimeUtils'; import type { ScheduleRun } from '../types/schedule'; import ScheduleStatusBadge from './StatusBadge'; @@ -10,10 +11,7 @@ export type RunDetailDrawerProps = { }; const formatTimestamp = (value?: string | null): string => { - if (!value) return '—'; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return value; - return d.toLocaleString(); + return value ? formatDateTime(value) : '—'; }; /** Side drawer with a single run's output / error (terminal-state only). */ diff --git a/frontend/src/agents/schedules/RunLog.tsx b/frontend/src/agents/schedules/RunLog.tsx index 364237b0..83b0913c 100644 --- a/frontend/src/agents/schedules/RunLog.tsx +++ b/frontend/src/agents/schedules/RunLog.tsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Button } from '../../components/ui/button'; import { selectToken } from '../../preferences/preferenceSlice'; import type { AppDispatch, RootState } from '../../store'; +import { formatDateTime } from '../../utils/dateTimeUtils'; import type { ScheduleRun } from '../types/schedule'; import ScheduleStatusBadge from './StatusBadge'; import { loadRunsForSchedule, selectRunsForSchedule } from './schedulesSlice'; @@ -14,10 +15,7 @@ export type RunLogProps = { }; const formatTimestamp = (value?: string | null): string => { - if (!value) return '—'; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return value; - return d.toLocaleString(); + return value ? formatDateTime(value) : '—'; }; /** Paginated run log for a schedule (SSE updates merge via schedulesSlice). */ diff --git a/frontend/src/agents/schedules/ScheduleFormModal.tsx b/frontend/src/agents/schedules/ScheduleFormModal.tsx index 1b7c48c4..d94d224d 100644 --- a/frontend/src/agents/schedules/ScheduleFormModal.tsx +++ b/frontend/src/agents/schedules/ScheduleFormModal.tsx @@ -20,6 +20,7 @@ import { TimePicker } from '@/components/ui/time-picker'; import { cn } from '@/lib/utils'; import { Modal } from '../../components/ui/modal'; +import { formatDateOnly } from '../../utils/dateTimeUtils'; import type { Schedule, ScheduleCreatePayload } from '../types/schedule'; import { browserTimezone, @@ -91,13 +92,7 @@ const dateToDateString = (d: Date): string => { }; const formatDateLabel = (value: string): string => { - const d = dateStringToDate(value); - if (!d) return ''; - return d.toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); + return formatDateOnly(value); }; /** Create/edit a Schedule via a modal dialog. */ diff --git a/frontend/src/agents/schedules/SchedulerToolCallCard.tsx b/frontend/src/agents/schedules/SchedulerToolCallCard.tsx index 0597b1c6..bbfc616b 100644 --- a/frontend/src/agents/schedules/SchedulerToolCallCard.tsx +++ b/frontend/src/agents/schedules/SchedulerToolCallCard.tsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Button } from '../../components/ui/button'; import { selectToken } from '../../preferences/preferenceSlice'; import type { AppDispatch } from '../../store'; +import { formatDateTime } from '../../utils/dateTimeUtils'; import { deleteSchedule, loadSchedulesForAgent } from './schedulesSlice'; export type SchedulerToolCallCardProps = { @@ -18,10 +19,7 @@ export type SchedulerToolCallCardProps = { }; const formatTimestamp = (value?: string | null): string => { - if (!value) return '—'; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return value; - return d.toLocaleString(); + return value ? formatDateTime(value) : '—'; }; const parseResult = (result: unknown): Record | null => { diff --git a/frontend/src/agents/schedules/SchedulesView.tsx b/frontend/src/agents/schedules/SchedulesView.tsx index 56d1b6fb..bc755f22 100644 --- a/frontend/src/agents/schedules/SchedulesView.tsx +++ b/frontend/src/agents/schedules/SchedulesView.tsx @@ -10,6 +10,7 @@ import ConfirmationModal from '../../modals/ConfirmationModal'; import { ActiveState } from '../../models/misc'; import { selectToken } from '../../preferences/preferenceSlice'; import type { AppDispatch, RootState } from '../../store'; +import { formatDateTime } from '../../utils/dateTimeUtils'; import AgentPageHeader from '../AgentPageHeader'; import type { Agent } from '../types'; import type { @@ -33,10 +34,7 @@ import { } from './schedulesSlice'; const formatTimestamp = (value?: string | null): string => { - if (!value) return '—'; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return value; - return d.toLocaleString(); + return value ? formatDateTime(value) : '—'; }; /** Standalone Schedules page for an agent: list, create, edit, pause, run, delete. */ @@ -170,7 +168,7 @@ export default function SchedulesView() { {agent.last_used_at ? t('agents.logs.lastUsedAt') + ' ' + - new Date(agent.last_used_at).toLocaleString() + formatDateTime(agent.last_used_at) : t('agents.logs.noUsageHistory')}

diff --git a/frontend/src/components/FilePicker.tsx b/frontend/src/components/FilePicker.tsx index ba0b4ffb..d8437d42 100644 --- a/frontend/src/components/FilePicker.tsx +++ b/frontend/src/components/FilePicker.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import { formatBytes } from '../utils/stringUtils'; -import { formatDate } from '../utils/dateTimeUtils'; +import { formatDateTime } from '../utils/dateTimeUtils'; import { getSessionToken, setSessionToken, @@ -599,7 +599,7 @@ export const FilePicker: React.FC = ({ - {formatDate(file.modifiedTime)} + {formatDateTime(file.modifiedTime)} {file.size ? formatBytes(file.size) : '-'} diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 133c51ea..3a732467 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -8,7 +8,7 @@ import MessageInput from '../components/MessageInput'; import { Button } from '../components/ui/button'; import { selectToken } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; -import { formatDate } from '../utils/dateTimeUtils'; +import { formatDateTime } from '../utils/dateTimeUtils'; import ConversationMessages from './ConversationMessages'; import { addQuery, @@ -69,7 +69,7 @@ export const SharedConversation = () => { setFetchedData({ queries: data.queries, title: data.title, - date: formatDate(data.timestamp), + date: formatDateTime(data.timestamp), identifier, }), ); diff --git a/frontend/src/settings/RemoteDeviceConfig.tsx b/frontend/src/settings/RemoteDeviceConfig.tsx index a037aa42..f8426410 100644 --- a/frontend/src/settings/RemoteDeviceConfig.tsx +++ b/frontend/src/settings/RemoteDeviceConfig.tsx @@ -30,6 +30,7 @@ import { import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState } from '../models/misc'; import { selectToken } from '../preferences/preferenceSlice'; +import { formatDateTime } from '../utils/dateTimeUtils'; import { UserToolType } from './types'; /** ms-since-last-seen threshold for the online pill. */ @@ -43,12 +44,7 @@ function isOnline(device: Device | null): boolean { } function formatTimestamp(value: string | null | undefined): string { - if (!value) return '-'; - try { - return new Date(value).toLocaleString(); - } catch { - return value; - } + return value ? formatDateTime(value) : '-'; } /** Compact relative span (e.g. "12s", "5m", "3h", "2d") since `value`. */ diff --git a/frontend/src/utils/dateTimeUtils.test.ts b/frontend/src/utils/dateTimeUtils.test.ts new file mode 100644 index 00000000..36184377 --- /dev/null +++ b/frontend/src/utils/dateTimeUtils.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { formatDate, formatDateOnly, formatDateTime } from './dateTimeUtils'; + +describe('dateTimeUtils', () => { + it('formats date-only values as DD/MM/YYYY', () => { + expect(formatDate('2026-05-21')).toBe('21/05/2026'); + expect(formatDateOnly('2026-05-21')).toBe('21/05/2026'); + }); + + it('formats local datetime strings with a 24-hour time', () => { + expect(formatDate('2026-05-21 14:30:00')).toBe('21/05/2026, 14:30'); + expect(formatDateTime('2026-05-21T14:30:00')).toBe('21/05/2026, 14:30'); + }); + + it('does not invent a midnight time for date-only values', () => { + expect(formatDateTime('2026-05-21')).toBe('21/05/2026'); + }); + + it('supports ISO timestamps with fractional seconds and offsets', () => { + const value = '2026-06-09T00:51:20.427827+00:00'; + const expected = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(new Date(value)); + + expect(formatDate(value)).toBe(expected); + expect(formatDateTime(value)).toBe(expected); + }); + + it('parses natural-language dates into DD/MM/YYYY', () => { + expect(formatDate('May 21, 2026')).toBe('21/05/2026'); + }); + + it('returns the original value when parsing fails', () => { + expect(formatDate('not a date')).toBe('not a date'); + expect(formatDateTime('still not a date')).toBe('still not a date'); + }); +}); diff --git a/frontend/src/utils/dateTimeUtils.ts b/frontend/src/utils/dateTimeUtils.ts index ce7a2c39..1f287cab 100644 --- a/frontend/src/utils/dateTimeUtils.ts +++ b/frontend/src/utils/dateTimeUtils.ts @@ -1,59 +1,70 @@ -export function formatDate(dateString: string): string { - if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(dateString)) { - // ISO 8601 format - const dateTime = new Date(dateString); - return dateTime.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) { - const dateTime = new Date(dateString); - return dateTime.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) { - const dateTime = new Date(dateString); - return dateTime.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { - // `new Date('YYYY-MM-DD')` parses as UTC midnight, so rendering it - // in a timezone west of UTC shows the previous day. Construct from - // parts so the date is interpreted in local time (matching the - // space-separated hourly buckets, which already parse as local). - const [year, month, day] = dateString.split('-').map(Number); - const date = new Date(year, month - 1, day); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - } else if ( - /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test( - dateString, - ) - ) { - // Format: "Fri, 08 Jul 2025 06:00:00 GMT" - const dateTime = new Date(dateString); - return dateTime.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } else { - return dateString; +type FormatMode = 'auto' | 'date' | 'dateTime'; + +const DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/; +const LOCAL_DATE_TIME_REGEX = + /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?$/; +const HAS_TIME_REGEX = /\d{1,2}:\d{2}/; + +const DATE_FORMATTER = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', +}); + +const DATE_TIME_FORMATTER = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, +}); + +function parseDateValue(value: string): Date | null { + const dateOnlyMatch = DATE_ONLY_REGEX.exec(value); + if (dateOnlyMatch) { + const [, year, month, day] = dateOnlyMatch; + return new Date(Number(year), Number(month) - 1, Number(day)); } + + const localDateTimeMatch = LOCAL_DATE_TIME_REGEX.exec(value); + if (localDateTimeMatch) { + const [, year, month, day, hour, minute, second = '0'] = localDateTimeMatch; + return new Date( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second), + ); + } + + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function formatDateValue(value: string, mode: FormatMode): string { + const parsed = parseDateValue(value); + if (!parsed) return value; + + const hasTime = HAS_TIME_REGEX.test(value); + const shouldIncludeTime = + mode === 'dateTime' ? hasTime : mode === 'auto' && hasTime; + + return shouldIncludeTime + ? DATE_TIME_FORMATTER.format(parsed) + : DATE_FORMATTER.format(parsed); +} + +export function formatDate(dateString: string): string { + return formatDateValue(dateString, 'auto'); +} + +export function formatDateOnly(dateString: string): string { + return formatDateValue(dateString, 'date'); +} + +export function formatDateTime(dateString: string): string { + return formatDateValue(dateString, 'dateTime'); }