mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 16:14:10 +00:00
Merge pull request #2545 from ManishMadan2882/main
(feat:dateTime) consistent d/m/y
This commit is contained in:
13 files changed
+135
-116
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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`. */
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
Reference in new issue
Block a user