From ba60ffb9540bd5a24082c46c56ca1e14ec526ed1 Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Mon, 24 Aug 2026 20:28:24 +0800 Subject: [PATCH] fix(grid): ignore unchanged cell blur commits --- .../src/composables/useDataGridEditor.ts | 29 +++++-- packages/app-tests/dataGridEditor.test.ts | 84 +++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index fea18a6b52..337aac67b6 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -5,7 +5,7 @@ import { coerceDataGridCellValue, dataGridCellEditorText } from "@/lib/dataGrid/ import { focusDataGridEditorWithoutScrolling, preserveDataGridScrollPosition } from "@/lib/dataGrid/dataGridEditorFocus"; import { normalizeDataGridSaveError } from "@/lib/dataGrid/dataGridSql"; import { rowStatusFilterAfterAddingRow, type RowStatusFilter } from "@/lib/dataGrid/gridRowStatus"; -import { type GridNewRowMeta, type GridNewRowPlacement } from "@/lib/dataGrid/gridNewRowPlacement"; +import type { GridNewRowMeta, GridNewRowPlacement } from "@/lib/dataGrid/gridNewRowPlacement"; import { supportsDataGridTransaction } from "@/lib/table/tableEditing"; import { useConnectionStore } from "@/stores/connectionStore"; import { useHistoryStore } from "@/stores/historyStore"; @@ -119,9 +119,7 @@ export interface UseDataGridEditorOptions { onResultPayloadMutated?: () => void; onCellValueChanged?: (rowId: number, columnIndex: number) => void; prepareFullReload?: () => void; - emit: { - (event: "reload", sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number): void; - }; + emit: (event: "reload", sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number) => void; } interface PendingChangesSnapshot { @@ -601,6 +599,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { }) as CellValue; } + function coerceCommittedCellValue(value: string, currentValue: CellValue | undefined, oldValue: CellValue | undefined, columnIndex: number): CellValue { + const editorText = dataGridCellEditorText({ + value: currentValue, + databaseType: resolvedDatabaseType.value, + columnInfo: tableColumnForGridColumn(columnIndex), + }); + // Keep the original CellValue when the editor text was not changed. This + // avoids turning a displayed value such as number 1 into string "1" when + // result and table metadata use different representations. + if (value === editorText) return currentValue ?? null; + return coerceCellValue(value, oldValue, columnIndex); + } + let isBatching = false; let batchUndoSnapshotPushed = false; let batchMutated = false; @@ -833,8 +844,14 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { } const oldVal = result.value.rows[item.sourceIndex]?.[col]; - const newVal = options.explicitValue !== undefined ? options.explicitValue : coerceCellValue(editValue.value, oldVal, col); - const changed = newVal !== item.data[col]; + const currentVal = item.data[col] ?? null; + const newVal = options.explicitValue !== undefined ? options.explicitValue : coerceCommittedCellValue(editValue.value, currentVal, oldVal, col); + const changed = newVal !== currentVal; + if (!changed) { + editingCell.value = null; + isCommitting = false; + return { changed: false, rowKind: "existing" }; + } if (newVal !== oldVal) { if (changed) pushUndoSnapshot(); if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map()); diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index 718039bf83..6ed3fd58bd 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -113,6 +113,7 @@ function createQuickEntryEditor(options: { filterRowsInGetRowItem?: boolean; supportsInsert?: boolean; save?: (changes: { dirtyRows: Map>; newRows: CellValue[][]; newRowMeta: Array<{ sourceIndex?: number; editedColumns?: number[] }> }) => Promise; + onCellValueChanged?: (rowId: number, columnIndex: number) => void; }) { const result = computed(() => ({ columns: ["id", "name"], @@ -143,6 +144,7 @@ function createQuickEntryEditor(options: { currentWhereInput: computed(() => undefined), rowStatusFilter, dataGridQuickEntryEnabled: computed(() => options.quickEntryEnabled), + onCellValueChanged: options.onCellValueChanged, pageSize: ref(50), currentPage: ref(1), cacheKey: options.cacheKey ? computed(() => options.cacheKey) : undefined, @@ -1663,6 +1665,88 @@ test("quick entry off keeps blur edits pending without saving", async () => { assert.equal(editor.dirtyRows.value.get(0)?.get(1), "Ada Lovelace"); }); +test("unchanged cell blur commits do not create pending changes", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + let callbackCount = 0; + const editor = createQuickEntryEditor({ + quickEntryEnabled: false, + onCellValueChanged: () => { + callbackCount += 1; + }, + }); + const version = editor.pendingChangesVersion.value; + const transactionActive = editor.transactionActive.value; + + editor.startEdit(0, 1); + await editor.commitEditFromBlur(); + + assert.equal(editor.dirtyRows.value.size, 0); + assert.equal(editor.hasPendingChanges.value, false); + assert.equal(editor.transactionActive.value, transactionActive); + assert.equal(editor.pendingChangesVersion.value, version); + assert.equal(callbackCount, 0); + assert.equal(editor.rowDataWithChanges([1, "Ada"], 0)[1], "Ada"); +}); + +test("restoring the original cell value before blur is a no-op", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + const editor = createQuickEntryEditor({ quickEntryEnabled: false }); + + editor.startEdit(0, 1); + editor.editValue.value = "Bob"; + editor.editValue.value = "Ada"; + await editor.commitEditFromBlur(); + + assert.equal(editor.dirtyRows.value.size, 0); + assert.equal(editor.hasPendingChanges.value, false); + assert.equal(editor.canUndoPendingChange.value, false); + assert.equal(editor.pendingChangesVersion.value, 0); + assert.equal(editor.rowDataWithChanges([1, "Ada"], 0)[1], "Ada"); +}); + +test("unchanged cell blur commits preserve other dirty cells", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + const editor = createQuickEntryEditor({ quickEntryEnabled: false }); + + editor.applyCellValue(0, 0, "2"); + editor.startEdit(0, 1); + await editor.commitEditFromBlur(); + + assert.deepEqual([...(editor.dirtyRows.value.get(0)?.entries() ?? [])], [[0, 2]]); + assert.equal(editor.hasPendingChanges.value, true); + assert.equal(editor.rowDataWithChanges([1, "Ada"], 0)[1], "Ada"); +}); + +test("changed cell blur commits remain pending", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + const editor = createQuickEntryEditor({ quickEntryEnabled: false }); + + editor.startEdit(0, 1); + editor.editValue.value = "Bob"; + await editor.commitEditFromBlur(); + + assert.equal(editor.dirtyRows.value.get(0)?.get(1), "Bob"); + assert.equal(editor.hasPendingChanges.value, true); + assert.equal(editor.rowDataWithChanges([1, "Ada"], 0)[1], "Bob"); +}); + +test("unchanged numeric cell blur commits keep the numeric baseline", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + const editor = createPeopleGridEditor(); + + editor.startEdit(0, 0); + await editor.commitEditFromBlur(); + + assert.equal(editor.dirtyRows.value.size, 0); + assert.equal(editor.hasPendingChanges.value, false); + assert.equal(editor.rowDataWithChanges([1, "Ada"], 0)[0], 1); +}); + test("explicit enum commits distinguish NULL, empty string, and the literal NULL", async () => { setActivePinia(createPinia()); installBrowserTestGlobals();