fix(ui): accept hour-only offsets when pasting a date-time (#15495)

Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
This commit is contained in:
Abhishek B R
2026-10-22 10:22:09 +00:00
committed by GitHub
co-authored by Abhay Padamani
parent addf3e4df5
commit e816c0974e
2 changed files with 31 additions and 6 deletions
+20
View File
@@ -78,6 +78,15 @@ describe("parseDateRangeString", () => {
});
});
it("accepts an hour-only offset (+HH), which date-fns renders for whole-hour zones", () => {
// 08:33:06-05 is the same instant as 13:33:06Z.
expect(parseDateRangeString("2026-07-21T08:33:06-05 - 2026-07-21T10:33:06-05")).toEqual({
type: "timestamp",
startMicros: 1784640786000000,
endMicros: 1784647986000000,
});
});
it("without an offset, resolves to the picker-timezone-dependent absolute shape", () => {
expect(parseDateRangeString("2026-07-21T13:33:06 - 2026-07-21T15:33:06")).toEqual({
type: "absolute",
@@ -399,6 +408,17 @@ describe("parseSingleDateTime", () => {
});
});
it("with an hour-only offset, as formatToHuman renders it in a whole-hour zone", () => {
expect(parseSingleDateTime("Jul 22, 2026 01:40:28.008 -05")).toEqual({
type: "timestamp",
micros: 1784702428008000,
});
expect(parseSingleDateTime("Jul 22, 2026 08:40:28.008 +02")).toEqual({
type: "timestamp",
micros: 1784702428008000,
});
});
it("without milliseconds", () => {
expect(parseSingleDateTime("Jul 22, 2026 06:40:28 +0000")).toEqual({
type: "timestamp",
+11 -6
View File
@@ -55,10 +55,10 @@ const TIMESTAMP_RANGE_RE = /^(\d{10,16})\s*-\s*(\d{10,16})$/;
const ABSOLUTE_SINGLE_RE = /^(\d{4}\/\d{2}\/\d{2})(?:\s+(\d{2}:\d{2}:\d{2}))?$/;
const TIMESTAMP_SINGLE_RE = /^(\d{10,16})$/;
const ISO_SINGLE_RE =
/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?(Z|[+-]\d{2}:?\d{2})?$/;
/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?(Z|[+-]\d{2}(?::?\d{2})?)?$/;
// The app's own log/trace timestamp display format (src/utils/date.ts formatTimestamp).
const HUMAN_LOG_SINGLE_RE =
/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),?\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:\s+(Z|[+-]\d{2}:?\d{2}))?$/i;
/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),?\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:\s+(Z|[+-]\d{2}(?::?\d{2})?))?$/i;
const MONTH_INDEX: Record<string, string> = {
jan: "01",
feb: "02",
@@ -74,6 +74,13 @@ const MONTH_INDEX: Record<string, string> = {
dec: "12",
};
// date-fns `X` (the app's `Z` token) renders whole-hour zones as `+HH`, with no minutes.
function normalizeOffset(offset: string): string {
if (offset.toUpperCase() === "Z") return "Z";
const [, hours, minutes = "00"] = offset.match(/^([+-]\d{2}):?(\d{2})?$/) ?? [];
return `${hours}:${minutes}`;
}
function isValidDateTimeParts(
year: string,
month: string,
@@ -208,8 +215,7 @@ function parseIsoSingle(raw: string): IsoSingleParsed | null {
if (offset) {
// Explicit offset already IS the instant — ignores the picker's timezone.
const normalizedOffset =
offset === "Z" ? "Z" : offset.replace(/^([+-]\d{2}):?(\d{2})$/, "$1:$2");
const normalizedOffset = normalizeOffset(offset);
const iso = `${year}-${month}-${day}T${hour ?? "00"}:${minute ?? "00"}:${second ?? "00"}${normalizedOffset}`;
const ms = Date.parse(iso);
if (Number.isNaN(ms)) return null;
@@ -234,8 +240,7 @@ function parseHumanLogSingle(raw: string): IsoSingleParsed | null {
if (!isValidDateTimeParts(year, month, dd, hour, minute, secWithMs)) return null;
if (offset) {
const normalizedOffset =
offset.toUpperCase() === "Z" ? "Z" : offset.replace(/^([+-]\d{2}):?(\d{2})$/, "$1:$2");
const normalizedOffset = normalizeOffset(offset);
const iso = `${year}-${month}-${dd}T${hour}:${minute}:${secWithMs}${normalizedOffset}`;
const parsedMs = Date.parse(iso);
if (Number.isNaN(parsedMs)) return null;