fix(grid): edit multi-cell selections inline
This commit is contained in:
6 files changed
+158
-13
No files matched your search
@@ -242,6 +242,7 @@ import { dataGridCanvasDevicePixelSize, useDataGridCanvasRuntime, type DataGridC
|
||||
import { useDataGridScrollbars, type DataGridScrollbarsRuntime } from "@/composables/useDataGridScrollbars";
|
||||
import { useDataGridSelection } from "@/composables/useDataGridSelection";
|
||||
import { dataGridNavigationOrigin, dataGridPageScrollTop, dataGridRowScrollTop, moveDataGridCell, navigateDataGridCell, type DataGridNavigationDirection, type DataGridScrollAlignment } from "@/lib/dataGrid/dataGridNavigation";
|
||||
import { dataGridInlineBulkEditValue } from "@/lib/dataGrid/dataGridInlineBulkEdit";
|
||||
import type { CellPosition } from "@/lib/dataGrid/gridSelection";
|
||||
import { createDataGridRuntimeScope } from "@/lib/dataGrid/dataGridRuntime";
|
||||
import { useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
@@ -822,6 +823,7 @@ function onGridContextMenuClose() {
|
||||
|
||||
const bulkEditDialogOpen = ref(false);
|
||||
const bulkEditValue = ref("");
|
||||
const inlineBulkEditActive = ref(false);
|
||||
const copyColumnNamesDialogOpen = ref(false);
|
||||
const copyColumnNamesDialogColumns = ref<string[]>([]);
|
||||
const insertRowsDialogOpen = ref(false);
|
||||
@@ -3699,13 +3701,16 @@ function cellUsesExpandedEditor(rowId: number | undefined, columnIndex: number):
|
||||
return !!expandedCellEditor.value && expandedCellEditor.value.rowId === rowId && expandedCellEditor.value.col === columnIndex;
|
||||
}
|
||||
|
||||
async function startCellEdit(rowId: number, columnIndex: number, expanded: boolean) {
|
||||
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
|
||||
async function startCellEdit(rowId: number, columnIndex: number, expanded: boolean, initialValue?: string): Promise<boolean> {
|
||||
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return false;
|
||||
closeReadonlyCellTextSelection();
|
||||
expandedCellEditor.value = expanded ? { rowId, col: columnIndex } : null;
|
||||
const item = getRowItem(rowId);
|
||||
startEdit(rowId, columnIndex);
|
||||
if (item) editValue.value = inlineCellEditorText(item.data[columnIndex] ?? null, columnIndex);
|
||||
startEdit(rowId, columnIndex, initialValue === undefined);
|
||||
const started = editingCell.value?.rowId === rowId && editingCell.value.col === columnIndex;
|
||||
if (!started) return false;
|
||||
editValue.value = initialValue ?? (item ? inlineCellEditorText(item.data[columnIndex] ?? null, columnIndex) : editValue.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function startDomCellEdit(rowId: number, columnIndex: number, displayText: string, event: MouseEvent) {
|
||||
@@ -7514,14 +7519,14 @@ function selectedVisibleColumnIndexes(): number[] {
|
||||
return [...selectedColumnIndexes.value].filter((index) => index >= 0 && index < visibleColumns.value.length).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function applyVisibleCellValue(item: RowItem, visibleCol: number, value: string | null, options: { preserveEmptyString?: boolean } = {}): boolean {
|
||||
function applyVisibleCellValue(item: RowItem, visibleCol: number, value: string | null, options: { preserveEmptyString?: boolean; emptyStringAsNull?: boolean } = {}): boolean {
|
||||
const actualCol = actualColumnIndex(visibleCol);
|
||||
if (!canEditCellItem(item, actualCol)) return false;
|
||||
applyCellValue(item.id, actualCol, value, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyVisibleSelectedCellValue(item: RowItem, visibleCol: number, value: string | null, allowDraft = selectedRangeTargetsOnlyDraftRow(), options: { preserveEmptyString?: boolean } = {}): boolean {
|
||||
function applyVisibleSelectedCellValue(item: RowItem, visibleCol: number, value: string | null, allowDraft = selectedRangeTargetsOnlyDraftRow(), options: { preserveEmptyString?: boolean; emptyStringAsNull?: boolean } = {}): boolean {
|
||||
if (!canApplyGridSelectionValue({ isDraft: !!item.isDraft, allowDraft })) return false;
|
||||
return applyVisibleCellValue(item, visibleCol, value, options);
|
||||
}
|
||||
@@ -7533,7 +7538,7 @@ function selectedRangeTargetsOnlyDraftRow(): boolean {
|
||||
return displayItemAt(range.startRow)?.isDraft === true;
|
||||
}
|
||||
|
||||
function fillSelectionWithValue(value: string | null): boolean {
|
||||
function fillSelectionWithValue(value: string | null, options: { preserveEmptyString?: boolean; emptyStringAsNull?: boolean } = {}): boolean {
|
||||
const range = selectedRange.value;
|
||||
let applied = false;
|
||||
const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow();
|
||||
@@ -7544,7 +7549,7 @@ function fillSelectionWithValue(value: string | null): boolean {
|
||||
const item = displayItemAt(rowIndex);
|
||||
if (!item) continue;
|
||||
for (let visibleCol = range.startCol; visibleCol <= range.endCol; visibleCol++) {
|
||||
applied = applyVisibleSelectedCellValue(item, visibleCol, value, allowDraftSelectionValue) || applied;
|
||||
applied = applyVisibleSelectedCellValue(item, visibleCol, value, allowDraftSelectionValue, options) || applied;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
@@ -7557,7 +7562,7 @@ function fillSelectionWithValue(value: string | null): boolean {
|
||||
const item = displayItemAt(rowIndex);
|
||||
if (!item) continue;
|
||||
for (const visibleCol of visibleColumnIndexes) {
|
||||
applied = applyVisibleSelectedCellValue(item, visibleCol, value) || applied;
|
||||
applied = applyVisibleSelectedCellValue(item, visibleCol, value, undefined, options) || applied;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
@@ -7639,6 +7644,18 @@ function editableSelectionCells(): EditableSelectionCell[] {
|
||||
return cells;
|
||||
}
|
||||
|
||||
function beginInlineBulkEdit(initialValue: string): boolean {
|
||||
if (!props.editable) return false;
|
||||
const target = editableSelectionCells()[0];
|
||||
if (!target || selectedCellCount.value <= 1) return false;
|
||||
const actualCol = actualColumnIndex(target.visibleCol);
|
||||
inlineBulkEditActive.value = true;
|
||||
void startCellEdit(target.item.id, actualCol, false, initialValue).then((started) => {
|
||||
if (!started) inlineBulkEditActive.value = false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyGeneratedSelectionValue(kind: CellValueGenerationKind, startValue = 1n): boolean {
|
||||
if (!props.editable) return false;
|
||||
const cells = editableSelectionCells();
|
||||
@@ -8038,9 +8055,20 @@ function currentIoTDBTimestampEditValue(): CellValue | undefined {
|
||||
|
||||
function commitGridEdit(value?: CellValue) {
|
||||
if (value === undefined) value = currentIoTDBTimestampEditValue();
|
||||
if (commitInlineBulkEdit(value)) return;
|
||||
void commitEditAndMaybeAutoSave(value === undefined ? undefined : { explicitValue: value }).finally(() => nextTick(() => gridRef.value?.focus({ preventScroll: true })));
|
||||
}
|
||||
|
||||
function commitInlineBulkEdit(value?: CellValue): boolean {
|
||||
if (!inlineBulkEditActive.value) return false;
|
||||
inlineBulkEditActive.value = false;
|
||||
const nextValue = value === undefined ? editValue.value : value === null ? null : String(value);
|
||||
cancelEdit();
|
||||
fillSelectionWithValue(nextValue, { emptyStringAsNull: true });
|
||||
nextTick(() => gridRef.value?.focus({ preventScroll: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function commitBooleanGridEdit(value?: string | null) {
|
||||
if (value === undefined) {
|
||||
commitGridEdit();
|
||||
@@ -8055,9 +8083,10 @@ function commitBooleanGridEdit(value?: string | null) {
|
||||
}
|
||||
|
||||
async function commitEditFromCellBlur() {
|
||||
const timestamp = currentIoTDBTimestampEditValue();
|
||||
if (commitInlineBulkEdit(timestamp)) return;
|
||||
const target = pendingQuickEntryDraftCellFocus.value;
|
||||
pendingQuickEntryDraftCellFocus.value = null;
|
||||
const timestamp = currentIoTDBTimestampEditValue();
|
||||
const timestampOptions = timestamp === undefined ? {} : { explicitValue: timestamp };
|
||||
if (target && editingCell.value?.rowId === quickEntryDraftRowId && target.rowId === quickEntryDraftRowId) {
|
||||
await commitEditFromBlur({ ...timestampOptions, promoteDraft: false });
|
||||
@@ -8110,13 +8139,25 @@ async function onCellEditKeydown(event: KeyboardEvent) {
|
||||
if (isSaveShortcut(event, settingsStore.editorSettings.shortcuts)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
commitEdit();
|
||||
if (!commitInlineBulkEdit(currentIoTDBTimestampEditValue())) commitEdit();
|
||||
await nextTick();
|
||||
if (await saveGridChangesFromShortcut()) {
|
||||
gridRef.value?.focus({ preventScroll: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (inlineBulkEditActive.value) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
commitInlineBulkEdit(currentIoTDBTimestampEditValue());
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
inlineBulkEditActive.value = false;
|
||||
cancelEdit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
onEditKeydown(event);
|
||||
}
|
||||
|
||||
@@ -8246,6 +8287,12 @@ async function onGridKeydown(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const inlineBulkEditValue = dataGridInlineBulkEditValue(event, selectedCellCount.value);
|
||||
if (inlineBulkEditValue !== undefined && beginInlineBulkEdit(inlineBulkEditValue)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter" && editSelectedCell()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -9212,6 +9259,7 @@ watch(editingCell, (cell) => {
|
||||
scheduleActiveCellEditTextareaResize();
|
||||
if (cell) nextTick(observeCellEditResizeBounds);
|
||||
else {
|
||||
inlineBulkEditActive.value = false;
|
||||
resetCellEditTextareaScrollOnResize = false;
|
||||
expandedCellEditor.value = null;
|
||||
disconnectCellEditResizeObserver();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dataGridInlineBulkEditValue } from "@/lib/dataGrid/dataGridInlineBulkEdit";
|
||||
|
||||
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string, nextName: string): string {
|
||||
const start = dataGridSource.indexOf(`function ${name}`);
|
||||
const end = dataGridSource.indexOf(`function ${nextName}`, start + 1);
|
||||
return start >= 0 && end > start ? dataGridSource.slice(start, end) : "";
|
||||
}
|
||||
|
||||
describe("DataGrid inline bulk editing", () => {
|
||||
it.each([
|
||||
["1", "1"],
|
||||
["删", "删"],
|
||||
["🙂", "🙂"],
|
||||
])("starts from the printable %j key", (key, expected) => {
|
||||
expect(dataGridInlineBulkEditValue({ key }, 2)).toBe(expected);
|
||||
});
|
||||
|
||||
it("starts empty with Enter and leaves single-cell editing unchanged", () => {
|
||||
expect(dataGridInlineBulkEditValue({ key: "Enter" }, 2)).toBe("");
|
||||
expect(dataGridInlineBulkEditValue({ key: "1" }, 1)).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([{ key: "v", ctrlKey: true }, { key: "1", altKey: true }, { key: "删", isComposing: true }, { key: "删", keyCode: 229 }, { key: "Process" }])("ignores shortcut, composition, and control keys: %j", (event) => {
|
||||
expect(dataGridInlineBulkEditValue(event, 2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("starts a cell editor instead of opening the bulk edit dialog", () => {
|
||||
const keydown = functionBody("onGridKeydown", "copyDetailValue");
|
||||
expect(keydown).toContain("beginInlineBulkEdit(inlineBulkEditValue)");
|
||||
expect(keydown).not.toContain("openBulkEditDialog");
|
||||
});
|
||||
|
||||
it("commits the inline value to the full selection", () => {
|
||||
const commit = functionBody("commitInlineBulkEdit", "commitBooleanGridEdit");
|
||||
expect(commit).toContain("fillSelectionWithValue(nextValue, { emptyStringAsNull: true })");
|
||||
expect(commit).toContain("cancelEdit()");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user