feat(dataGrid): fix temporal cell editor precision
This commit is contained in:
5 files changed
+224
-29
No files matched your search
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user