Merge pull request #2545 from ManishMadan2882/main

(feat:dateTime) consistent d/m/y
This commit is contained in:
Alex authored and GitHub committed 2026-06-16 17:27:47 +01:00
commit 0ced2d734c
13 files changed
+135 -116

No files matched your search

+3 -12
View File
@@ -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 {
+2 -1
View File
@@ -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')}
</p>
</div>
+2 -12
View File
@@ -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 && (
<p className="text-muted-foreground text-xs font-light sm:text-sm">
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)}
</p>
)}
</div>
@@ -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). */
+2 -4
View File
@@ -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). */
@@ -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. */
@@ -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<string, unknown> | null => {
@@ -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')}
</p>
</div>
+2 -2
View File
@@ -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<CloudFilePickerProps> = ({
</div>
</TableCell>
<TableCell className="text-xs">
{formatDate(file.modifiedTime)}
{formatDateTime(file.modifiedTime)}
</TableCell>
<TableCell className="text-xs">
{file.size ? formatBytes(file.size) : '-'}
@@ -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,
}),
);
+2 -6
View File
@@ -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`. */
+43
View File
@@ -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');
});
});
+68 -57
View File
@@ -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');
}