feat(dataGrid): fix temporal cell editor precision

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-09 20:20:14 +08:00
1 parent 0e474cecd4
commit 90276464b2
5 files changed
+224 -29

No files matched your search

+50 -10
View File
@@ -115,7 +115,7 @@ import { canFormatCellDetailJson, cellDetailEditorText, compactJsonText, default
import { renderWktOnCanvas, isHexGeometry } from "@/lib/dataGrid/geometryPreview";
import { buildDataGridCellDetail, buildDataGridColumnDetail, buildDataGridRowDetail, dataGridColumnDetailJson, dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, filterDataGridDetailFields, type DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
import { applyColumnFormatter, buildColumnFormatterKey, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit, DateTimePatterns } from "@/lib/dataGrid/columnFormatter";
import { temporalCellEditorKind, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor";
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";
import { canGoNextDataGridPage } from "@/lib/dataGrid/dataGridPagination";
@@ -4066,8 +4066,8 @@ function resultColumnInfoForGridColumn(columnIndex: number): Pick<ColumnInfo, "d
return dataType ? { data_type: dataType } : undefined;
}
function temporalEditorKindForColumn(columnIndex: number): TemporalCellEditorKind | undefined {
return temporalCellEditorKind(tableColumnForGridColumn(columnIndex)?.data_type, props.databaseType);
function temporalEditorConfigForColumn(columnIndex: number): TemporalCellEditorConfig | undefined {
return temporalCellEditorConfig(tableColumnForGridColumn(columnIndex), props.databaseType);
}
function enumValuesForGridColumn(columnIndex: number): string[] {
@@ -4928,9 +4928,9 @@ watch(activeCellDetail, (detail) => {
const detailEditValue = ref("");
const isEditingDetail = ref(false);
const detailTemporalEditorKind = computed(() => {
const detailTemporalEditorConfig = computed(() => {
const detail = activeCellDetail.value;
return detail ? temporalEditorKindForColumn(detail.colIndex) : undefined;
return detail ? temporalEditorConfigForColumn(detail.colIndex) : undefined;
});
const sideDetailPrioritizesValue = computed(() => !cellDetailPanelIsBottom.value && !isEditingDetail.value && !!activeCellDetail.value?.formattedJson);
const sideDetailValueFillsHeight = computed(() => cellDetailPanelIsBottom.value || isEditingDetail.value || (!cellDetailPanelIsBottom.value && !activeCellDetail.value?.imagePreviewUrl));
@@ -9197,7 +9197,15 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@dblclick.stop="canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && startDomCellEdit(displayItems[cell.recordIndex].id, cell.valueIndex, cell.display, $event)"
>
<template v-if="editingCell?.rowId === displayItems[cell.recordIndex]?.id && editingCell?.col === cell.valueIndex">
<TemporalCellEditor v-if="temporalEditorKindForColumn(cell.valueIndex)" v-model="editValue" :kind="temporalEditorKindForColumn(cell.valueIndex)!" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
<TemporalCellEditor
v-if="temporalEditorConfigForColumn(cell.valueIndex)"
v-model="editValue"
:kind="temporalEditorConfigForColumn(cell.valueIndex)!.kind"
:fraction-precision="temporalEditorConfigForColumn(cell.valueIndex)!.fractionPrecision"
cell-layout="transpose"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<EnumCellEditor v-else-if="isEnumGridColumn(cell.valueIndex)" v-model="editValue" :values="enumValuesForGridColumn(cell.valueIndex)" :nullable="isEnumGridColumnNullable(cell.valueIndex)" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
<textarea
v-else-if="cellUsesExpandedEditor(displayItems[cell.recordIndex]?.id, cell.valueIndex)"
@@ -9672,7 +9680,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
/>
<div ref="canvasOverlayRef" class="canvas-grid-overlay dbx-data-grid-font-family sticky left-0 top-0 z-10 overflow-visible" :style="canvasOverlayStyle">
<div v-if="canvasEditingCell" class="absolute pointer-events-auto z-20 tabular-nums" :style="canvasEditingCellStyle" @mousedown.stop @click.stop>
<TemporalCellEditor v-if="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
<TemporalCellEditor
v-if="temporalEditorConfigForColumn(canvasEditingCell.actualColIdx)"
v-model="editValue"
:kind="temporalEditorConfigForColumn(canvasEditingCell.actualColIdx)!.kind"
:fraction-precision="temporalEditorConfigForColumn(canvasEditingCell.actualColIdx)!.fractionPrecision"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<EnumCellEditor
v-else-if="isEnumGridColumn(canvasEditingCell.actualColIdx)"
v-model="editValue"
@@ -9815,7 +9830,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx, $event)"
>
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
<TemporalCellEditor v-if="temporalEditorKindForColumn(col.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(col.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
<TemporalCellEditor
v-if="temporalEditorConfigForColumn(col.actualColIdx)"
v-model="editValue"
:kind="temporalEditorConfigForColumn(col.actualColIdx)!.kind"
:fraction-precision="temporalEditorConfigForColumn(col.actualColIdx)!.fractionPrecision"
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<EnumCellEditor v-else-if="isEnumGridColumn(col.actualColIdx)" v-model="editValue" :values="enumValuesForGridColumn(col.actualColIdx)" :nullable="isEnumGridColumnNullable(col.actualColIdx)" @cancel="cancelEdit" @commit="commitGridEdit" />
<textarea
v-else-if="cellUsesExpandedEditor(item.id, col.actualColIdx)"
@@ -10238,7 +10260,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
<template v-if="isEditingDetail">
<div class="min-h-0 flex-1" :style="sideDetailEditorStyle">
<TemporalCellEditor v-if="detailTemporalEditorKind" v-model="detailEditValue" :kind="detailTemporalEditorKind" variant="inline" :commit-on-close="false" @cancel="cancelDetailEdit" @commit="commitDetailEdit" />
<TemporalCellEditor
v-if="detailTemporalEditorConfig"
v-model="detailEditValue"
:kind="detailTemporalEditorConfig.kind"
:fraction-precision="detailTemporalEditorConfig.fractionPrecision"
variant="inline"
:commit-on-close="false"
@cancel="cancelDetailEdit"
@commit="commitDetailEdit"
/>
<div v-else ref="detailsEditorContainer" data-cell-detail-editor-root class="min-h-0 h-full w-full rounded border overflow-hidden" />
</div>
<div v-if="!cellDetailPanelIsBottom" class="flex shrink-0 gap-1 py-0.5">
@@ -10316,7 +10347,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<TabsContent v-if="activeCellDetailTabs.includes('valueEditor')" value="valueEditor" class="m-0 min-h-0 flex-1 flex flex-col p-3 text-xs">
<div class="flex min-h-0 flex-1 flex-col">
<TemporalCellEditor v-if="detailTemporalEditorKind" v-model="detailEditValue" :kind="detailTemporalEditorKind" variant="inline" :commit-on-close="false" @cancel="cancelValueEditorEdit" @commit="commitValueEditorEdit" />
<TemporalCellEditor
v-if="detailTemporalEditorConfig"
v-model="detailEditValue"
:kind="detailTemporalEditorConfig.kind"
:fraction-precision="detailTemporalEditorConfig.fractionPrecision"
variant="inline"
:commit-on-close="false"
@cancel="cancelValueEditorEdit"
@commit="commitValueEditorEdit"
/>
<div v-else ref="valueEditorContainer" data-cell-detail-editor-root class="min-h-0 flex-1 w-full rounded border overflow-auto" />
</div>
<div class="flex gap-1 mt-2 shrink-0">
@@ -13,11 +13,13 @@ const props = withDefaults(
variant?: "cell" | "inline";
cellLayout?: "grid" | "transpose";
commitOnClose?: boolean;
fractionPrecision?: number;
}>(),
{
variant: "cell",
cellLayout: "grid",
commitOnClose: true,
fractionPrecision: 0,
},
);
@@ -37,6 +39,14 @@ let skipCommitOnClose = false;
const hasDate = computed(() => props.kind !== "time");
const hasTime = computed(() => props.kind !== "date");
const normalizedFractionPrecision = computed(() => Math.max(0, Math.min(9, props.fractionPrecision || 0)));
const existingFractionDigits = computed(() => parseFractionDigits(timeValue.value));
const fractionInputLength = computed(() => normalizedFractionPrecision.value || existingFractionDigits.value.length);
const fractionInputMaxLength = computed(() => Math.max(1, fractionInputLength.value));
const showFractionInput = computed(() => hasTime.value && (normalizedFractionPrecision.value > 0 || existingFractionDigits.value.length > 0));
const timeGridStyle = computed(() => ({
gridTemplateColumns: showFractionInput.value ? "3.5rem 0.5rem 3.5rem 0.5rem 3.5rem 0.5rem 5rem" : "3.5rem 0.5rem 3.5rem 0.5rem 3.5rem",
}));
const editorRootClass = computed(() => (props.variant === "inline" ? "relative h-9 w-full" : "absolute inset-0 z-10"));
const inputClass = computed(() =>
props.variant === "inline"
@@ -64,7 +74,9 @@ const timeValue = computed(() => {
});
const timeParts = computed(() => {
const [hour = "00", minute = "00", second = "00"] = timeValue.value.split(":");
const match = /^(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?$/.exec(timeValue.value);
if (!match) return { hour: "00", minute: "00", second: "00" };
const [, hour = "00", minute = "00", second = "00"] = match;
return { hour, minute, second };
});
@@ -118,7 +130,7 @@ function stepDate(part: "day" | "month" | "year", delta: number) {
function updateTime(part: "hour" | "minute" | "second", rawValue: string | number) {
const parts = { ...timeParts.value, [part]: normalizeTimePart(rawValue, part === "hour" ? 23 : 59) };
const nextTime = `${parts.hour}:${parts.minute}:${parts.second}`;
const nextTime = `${parts.hour}:${parts.minute}:${parts.second}${fractionSuffix.value}`;
if (props.kind === "time") {
setModelValue(nextTime);
return;
@@ -134,6 +146,26 @@ function stepTime(part: "hour" | "minute" | "second", delta: number) {
setModelValue(stepTemporalInputValue(localValue.value, props.kind, part, delta));
}
const fractionSuffix = computed(() => {
const digits = existingFractionDigits.value;
return digits ? `.${digits}` : "";
});
function updateFraction(event: Event) {
updateFractionValue((event.target as HTMLInputElement).value);
}
function updateFractionValue(rawValue: string) {
const maxLength = Math.max(1, fractionInputLength.value);
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);
return;
}
setDateTimeValue(dateParts.value.year, dateParts.value.month, dateParts.value.day, nextTime);
}
function flushInputValue(target: EventTarget | null) {
if (!(target instanceof HTMLInputElement)) return;
const part = target.dataset.temporalPart;
@@ -141,6 +173,8 @@ function flushInputValue(target: EventTarget | null) {
updateDate(part, target.value);
} else if (part === "hour" || part === "minute" || part === "second") {
updateTime(part, target.value);
} else if (part === "fraction") {
updateFractionValue(target.value);
}
}
@@ -155,7 +189,7 @@ function setNull() {
function setNow() {
const now = new Date();
const dateText = [String(now.getFullYear()).padStart(4, "0"), String(now.getMonth() + 1).padStart(2, "0"), String(now.getDate()).padStart(2, "0")].join("-");
const nextTime = [String(now.getHours()).padStart(2, "0"), String(now.getMinutes()).padStart(2, "0"), String(now.getSeconds()).padStart(2, "0")].join(":");
const nextTime = [String(now.getHours()).padStart(2, "0"), String(now.getMinutes()).padStart(2, "0"), String(now.getSeconds()).padStart(2, "0")].join(":") + nowFractionSuffix(now);
if (props.kind === "date") setModelValue(dateText);
else if (props.kind === "time") setModelValue(nextTime);
else setModelValue(`${dateText} ${nextTime}`);
@@ -234,6 +268,16 @@ function setDateTimeValue(year: number, month: number, day: number, time: string
else setModelValue(`${dateText} ${time}`);
}
function parseFractionDigits(value: string): string {
return value.match(/\.(\d{1,9})/)?.[1] ?? "";
}
function nowFractionSuffix(now: Date): string {
const precision = normalizedFractionPrecision.value;
if (precision <= 0) return "";
return `.${String(now.getMilliseconds()).padStart(3, "0").padEnd(precision, "0").slice(0, precision)}`;
}
function daysInMonth(year: number, month: number): number {
return new Date(year, month, 0).getDate();
}
@@ -295,7 +339,7 @@ function twoDigit(value: string | number): string {
</div>
</div>
<div v-if="hasTime" class="grid grid-cols-[3.5rem_0.5rem_3.5rem_0.5rem_3.5rem] items-center gap-1.5">
<div v-if="hasTime" class="grid items-center gap-1.5" :style="timeGridStyle">
<div class="grid h-7 min-w-0 grid-cols-[1fr_1.35rem] overflow-hidden rounded-md border border-input bg-background">
<input :value="twoDigit(timeParts.hour)" data-temporal-part="hour" inputmode="numeric" class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none" @input="updateTimeFromInput('hour', $event)" />
<div class="grid border-l">
@@ -331,6 +375,12 @@ function twoDigit(value: string | number): string {
</button>
</div>
</div>
<template v-if="showFractionInput">
<span class="text-center text-xs text-muted-foreground">.</span>
<div class="grid h-7 min-w-0 overflow-hidden rounded-md border border-input bg-background">
<input :value="existingFractionDigits" data-temporal-part="fraction" inputmode="numeric" :maxlength="fractionInputMaxLength" class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none" placeholder="0" @input="updateFraction" />
</div>
</template>
</div>
<div class="flex items-center justify-between gap-1">