fix(postgres): render temporal values in session timezone

This commit is contained in:
jearhe authored and GitHub committed 2026-09-13 19:54:02 +08:00
1 parent a33768ef6e
commit ba77d94815
7 files changed
+329 -40

No files matched your search

@@ -4,7 +4,7 @@ import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor";
import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, temporalOffsetSuffix, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor";
const props = withDefaults(
defineProps<{
@@ -134,7 +134,7 @@ function updateTime(part: "hour" | "minute" | "second", rawValue: string | numbe
const parts = { ...timeParts.value, [part]: normalizeTimePart(rawValue, part === "hour" ? 23 : 59) };
const nextTime = `${parts.hour}:${parts.minute}:${parts.second}${fractionSuffix.value}`;
if (props.kind === "time") {
setModelValue(nextTime, true);
setModelValue(`${nextTime}${temporalOffsetSuffix(localValue.value, props.kind)}`, true);
return;
}
setDateTimeValue(dateParts.value.year, dateParts.value.month, dateParts.value.day, nextTime);
@@ -162,7 +162,7 @@ function updateFractionValue(rawValue: string) {
const digits = rawValue.replace(/\D/g, "").slice(0, maxLength);
const nextTime = `${timeParts.value.hour}:${timeParts.value.minute}:${timeParts.value.second}${digits ? `.${digits}` : ""}`;
if (props.kind === "time") {
setModelValue(nextTime, true);
setModelValue(`${nextTime}${temporalOffsetSuffix(localValue.value, props.kind)}`, true);
return;
}
setDateTimeValue(dateParts.value.year, dateParts.value.month, dateParts.value.day, nextTime);
@@ -267,7 +267,7 @@ function normalizeTimePart(value: string | number, max: number): string {
function setDateTimeValue(year: number, month: number, day: number, time: string) {
const dateText = [String(year).padStart(4, "0"), String(month).padStart(2, "0"), String(day).padStart(2, "0")].join("-");
if (props.kind === "date") setModelValue(dateText, true);
else setModelValue(`${dateText} ${time}`, true);
else setModelValue(`${dateText} ${time}${temporalOffsetSuffix(localValue.value, props.kind)}`, true);
}
function parseFractionDigits(value: string): string {
@@ -40,6 +40,15 @@ describe("dataGridTemporalEditor", () => {
expect(stepTemporalInputValue(value, "datetime", "second", 1)).toBe("2026-07-09 12:34:57.123456");
});
it("preserves timezone offsets when parsing or stepping temporal values", () => {
const datetime = "2026-07-09 12:34:56.123456+12:00";
const time = "12:34:56.123456-05:30";
expect(parseTemporalInputValue(time, "time")).toBe(time);
expect(stepTemporalInputValue(datetime, "datetime", "hour", 1)).toBe("2026-07-09 13:34:56.123456+12:00");
expect(stepTemporalInputValue(time, "time", "minute", 1)).toBe("12:35:56.123456-05:30");
});
it("keeps ordinary datetime values at second precision", () => {
expect(temporalCellEditorConfig("datetime")).toEqual({ kind: "datetime", fractionPrecision: 0 });
expect(formatTemporalInputValue("2026-07-09 12:34:56", "datetime")).toBe("2026-07-09T12:34:56");
@@ -56,7 +56,10 @@ export function parseTemporalInputValue(value: string, kind: TemporalCellEditorK
const text = value.trim();
if (!text) return null;
if (kind === "date") return text;
if (kind === "time") return normalizeTimeInput(text) || text;
if (kind === "time") {
const normalized = normalizeTimeInput(text);
return normalized ? `${normalized}${temporalOffsetSuffix(text, kind)}` : text;
}
const datetime = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(?::(\d{2})(\.\d{1,9})?)?$/);
if (!datetime) return text;
@@ -65,9 +68,20 @@ export function parseTemporalInputValue(value: string, kind: TemporalCellEditorK
export type TemporalCellEditorPart = "year" | "month" | "day" | "hour" | "minute" | "second";
export function temporalOffsetSuffix(value: string, kind: TemporalCellEditorKind): string {
const text = value.trim();
const offset = text.match(/(Z|[+-]\d{2}(?::?\d{2})?)$/i)?.[1];
if (!offset) return "";
const withoutOffset = text.slice(0, -offset.length);
const basePattern = kind === "time" ? /^\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/ : /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;
return basePattern.test(withoutOffset) ? offset : "";
}
export function stepTemporalInputValue(value: string, kind: TemporalCellEditorKind, part: TemporalCellEditorPart, delta: number): string {
const dateParts = temporalDateParts(value);
const timeParts = temporalTimeParts(value, kind);
const offset = temporalOffsetSuffix(value, kind);
if (part === "year") dateParts.year = Math.max(1, Math.min(9999, dateParts.year + delta));
else if (part === "month") dateParts.month = Math.max(1, Math.min(12, dateParts.month + delta));
@@ -82,8 +96,8 @@ export function stepTemporalInputValue(value: string, kind: TemporalCellEditorKi
const timeText = [timeParts.hour, timeParts.minute, timeParts.second].map((item) => String(item).padStart(2, "0")).join(":") + timeParts.fraction;
if (kind === "date") return dateText;
if (kind === "time") return timeText;
return `${dateText} ${timeText}`;
if (kind === "time") return `${timeText}${offset}`;
return `${dateText} ${timeText}${offset}`;
}
function normalizeTemporalType(dataType: string | undefined): string {