feat(grid): add timezone-aware column formatting

This commit is contained in:
ptma authored and GitHub committed 2026-07-16 18:42:33 +08:00
1 parent 41e0989f96
commit 9efdf79ad9
12 files changed
+143 -33

No files matched your search

+29 -4
View File
@@ -116,7 +116,7 @@ import {
import { buildBinaryHexViewRows } from "@/lib/dataGrid/binaryHexViewer";
import { canFormatCellDetailJson, cellDetailEditorText, compactJsonText, defaultCellDetailTab, formatJsonText, isGeometryColumnType, linkedCellDetailTarget, looksLikeJsonContainerText, valueEditorActions, visibleCellDetailTabs, type CellDetailTab } from "@/lib/dataGrid/cellDetailPresentation";
import { buildDataGridCellDetail, buildDataGridColumnDetail, buildDataGridRowDetail, CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH, dataGridColumnDetailJson, dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, type DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
import { applyColumnFormatter, buildColumnFormatterKey, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit, DateTimePatterns } from "@/lib/dataGrid/columnFormatter";
import { applyColumnFormatter, buildColumnFormatterKey, getSupportedTimeZoneOptions, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit, DateTimePatterns } from "@/lib/dataGrid/columnFormatter";
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
@@ -197,6 +197,12 @@ import { isMacOS } from "@/lib/backend/platform";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
import { SearchableSelect } from "@/components/ui/searchable-select";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import dayjs from "dayjs";
dayjs.extend(utc);
dayjs.extend(timezone);
const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue"));
const ImagePreviewDialog = defineAsyncComponent(() => import("@/components/grid/ImagePreviewDialog.vue"));
@@ -636,6 +642,8 @@ const CUSTOM_FORMATTER_NEW = "__new";
const formatterKind = ref<FormatterDraftKind>("datetime");
const formatterDateUnit = ref<DateTimeFormatterUnit>("auto");
const formatterDatetimePattern = ref<string>("YYYY-MM-DD HH:mm:ss");
const formatterDateTimezone = ref<string>(dayjs.tz.guess() || "UTC");
const timezoneOptions = getSupportedTimeZoneOptions(Intl as typeof Intl & { supportedValuesOf?: (key: "timeZone") => string[] }, formatterDateTimezone.value);
const formatterJsonPath = ref("$.user.name");
const formatterMaskPrefix = ref(4);
const formatterMaskSuffix = ref(4);
@@ -995,15 +1003,16 @@ function currentFormatterDraft(): ColumnFormatterConfig {
if (formatterKind.value === "custom-template") {
return { kind: "custom-template", template: formatterCustomTemplate.value.trim() || "${value}" };
}
return { kind: "datetime", unit: formatterDateUnit.value, pattern: formatterDatetimePattern.value };
return { kind: "datetime", unit: formatterDateUnit.value, pattern: formatterDatetimePattern.value, timezone: formatterDateTimezone.value };
}
function loadFormatterDraft(formatter: ColumnFormatterConfig | undefined) {
const draft = formatter ?? { kind: "datetime", unit: "auto" as const, pattern: "YYYY-MM-DD HH:mm:ss" as const };
const draft = formatter ?? { kind: "datetime", unit: "auto" as const, pattern: "YYYY-MM-DD HH:mm:ss" as const, timezone: dayjs.tz.guess() };
formatterKind.value = draft.kind === "custom-ref" ? "custom-template" : draft.kind;
if (draft.kind === "datetime") {
formatterDateUnit.value = draft.unit;
formatterDatetimePattern.value = draft.pattern;
formatterDateTimezone.value = draft.timezone || dayjs.tz.guess() || "UTC";
} else if (draft.kind === "json-path") {
formatterJsonPath.value = draft.path;
} else if (draft.kind === "mask") {
@@ -7743,7 +7752,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<Code2 class="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent align="start" side="bottom" class="w-[420px] max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl border bg-popover p-0 text-popover-foreground shadow-xl" @click.stop @keydown.stop>
<PopoverContent align="start" side="bottom" class="w-[450px] max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl border bg-popover p-0 text-popover-foreground shadow-xl" @click.stop @keydown.stop>
<div class="border-b bg-muted/40 px-3 py-2">
<div class="text-sm font-semibold">
{{ t("grid.columnFormatterFor", { column: col.name }) }}
@@ -7804,6 +7813,22 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@update:model-value="(value: any) => (formatterDatetimePattern = value)"
/>
</div>
<div class="space-y-1.5 flex-1">
<div class="text-xs font-medium text-muted-foreground">
{{ t("grid.formatterDatetimeTimezone") }}
</div>
<SearchableSelect
:model-value="formatterDateTimezone"
:options="timezoneOptions"
:placeholder="t('grid.formatterDatetimeTimezonePlaceholder')"
:search-placeholder="t('grid.formatterDatetimeTimezonePlaceholder')"
:empty-text="t('grid.formatterDatetimeTimezonePlaceholder')"
:loading-text="t('common.loading')"
:trigger-class="['border border-input h-8 pl-2.5 text-xs']"
item-class="h-auto min-h-8 px-2 py-1.5 text-xs"
@update:model-value="(value: any) => (formatterDateTimezone = value)"
/>
</div>
</div>
<div v-else-if="formatterKind === 'json-path'" class="space-y-1.5">
@@ -243,7 +243,7 @@ function handleKeydown(event: KeyboardEvent) {
</PopoverTrigger>
<PopoverContent :align="SEARCHABLE_SELECT_HELP_PANEL_ALIGN" :class="cn('w-auto max-w-[calc(100vw-1rem)] border-0 bg-transparent p-0 shadow-none ring-0', contentClass)">
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start">
<div ref="listCard" :class="cn('w-52 shrink-0 rounded-md border bg-popover p-1.5 shadow-md', listClass)">
<div ref="listCard" :class="cn('shrink-0 rounded-md border bg-popover p-1.5 shadow-md', listClass)">
<div class="relative rounded-sm border bg-background">
<Search class="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>