fix(grid): refresh database-computed values after edits

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-18 15:31:15 +08:00
1 parent b3b6d29099
commit 5acdbfa61e
5 files changed
+332 -12

No files matched your search

@@ -1,18 +1,26 @@
import { computed, ref } from "vue";
import { computed, ref, type Ref } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { clearDataGridPendingSnapshot, DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor";
import type { CellValue } from "@/lib/dataGrid/cellValue";
const mocks = vi.hoisted(() => ({
getConfig: vi.fn(),
prepareDataGridSave: vi.fn(),
executeBatch: vi.fn(),
executeInTransaction: vi.fn(),
addHistory: vi.fn(),
}));
vi.mock("@/lib/backend/api", () => ({}));
vi.mock("@/lib/backend/api", () => ({
prepareDataGridSave: mocks.prepareDataGridSave,
executeBatch: mocks.executeBatch,
executeInTransaction: mocks.executeInTransaction,
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({ getConfig: mocks.getConfig }),
}));
vi.mock("@/stores/historyStore", () => ({
useHistoryStore: () => ({}),
useHistoryStore: () => ({ add: mocks.addHistory }),
}));
vi.mock("@/stores/productionSafetyStore", () => ({
useProductionSafetyStore: () => ({}),
@@ -294,3 +302,123 @@ describe("useDataGridEditor appendPastedRowsToNewRow", () => {
expect(editor.newRows.value[1]).toEqual(["Ada", "full payload", "Lovelace"]);
});
});
describe("useDataGridEditor saveChanges reload", () => {
beforeEach(() => {
mocks.prepareDataGridSave.mockReset();
mocks.executeBatch.mockReset();
mocks.executeInTransaction.mockReset();
mocks.addHistory.mockReset();
mocks.getConfig.mockReset();
});
function createSaveTestEditor(options: { currentPage?: Ref<number>; prepareFullReload?: () => void; customSaveHandler?: { save: ReturnType<typeof vi.fn> } } = {}) {
const emit = vi.fn();
const currentPage = options.currentPage ?? ref(1);
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
columns: ["id", "status"],
rows: [
[1, "pending"],
[2, "pending"],
],
});
const editor = useDataGridEditor({
result: computed(() => result.value),
editable: computed(() => true),
databaseType: computed(() => "mysql"),
connectionId: computed(() => "connection-1"),
database: computed(() => "app"),
tableMeta: computed(() => ({
tableName: "orders_test",
columns: [
{ name: "id", data_type: "int" },
{ name: "status", data_type: "varchar" },
],
primaryKeys: ["id"],
})),
sourceColumns: computed(() => undefined),
onExecuteSql: computed(() => undefined),
customSaveHandler: computed(() => options.customSaveHandler),
sql: computed(() => undefined),
searchText: ref(""),
whereFilterInput: ref(""),
currentWhereInput: computed(() => undefined),
orderByInput: ref(""),
rowStatusFilter: ref("all"),
confirmDangerousRowDeletion: computed(() => true),
pageSize: ref(100),
currentPage,
cacheKey: computed(() => undefined),
getRowItem: () => undefined,
prepareFullReload: options.prepareFullReload,
emit,
});
return { editor, emit, currentPage };
}
it("reloads after a pure row update, so database-computed columns (e.g. ON UPDATE CURRENT_TIMESTAMP) refresh without a manual page reload", async () => {
mocks.prepareDataGridSave.mockResolvedValue({ statements: ["UPDATE orders_test SET status='shipped' WHERE id=1"], rollbackStatements: [] });
mocks.executeBatch.mockResolvedValue({ affected_rows: 1 });
const { editor, emit } = createSaveTestEditor();
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(mocks.executeBatch).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith("reload", undefined, "", undefined, undefined, 100, 0);
});
it("does not reload when there are no pending changes to save", async () => {
const { editor, emit } = createSaveTestEditor();
await editor.saveChanges();
expect(mocks.prepareDataGridSave).not.toHaveBeenCalled();
expect(emit).not.toHaveBeenCalledWith("reload", expect.anything());
});
it("prepares one first-page reload after saving edits from three accumulated infinite-scroll pages", async () => {
mocks.prepareDataGridSave.mockResolvedValue({
statements: ["UPDATE orders_test SET status='shipped' WHERE id=1", "UPDATE orders_test SET status='cancelled' WHERE id=2"],
rollbackStatements: [],
});
mocks.executeInTransaction.mockResolvedValue({ affected_rows: 2 });
const infiniteScrollState = {
lastPage: 3,
requestedOffset: 200 as number | undefined,
requestedLimit: 100 as number | undefined,
};
const currentPage = ref(3);
const prepareFullReload = vi.fn(() => {
currentPage.value = 1;
infiniteScrollState.lastPage = 0;
infiniteScrollState.requestedOffset = undefined;
infiniteScrollState.requestedLimit = undefined;
});
const created = createSaveTestEditor({ currentPage, prepareFullReload });
created.editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
created.editor.dirtyRows.value.set(1, new Map([[1, "cancelled"]]));
await created.editor.saveChanges();
expect(mocks.executeInTransaction).toHaveBeenCalledTimes(1);
expect(prepareFullReload).toHaveBeenCalledTimes(1);
expect(infiniteScrollState).toEqual({ lastPage: 0, requestedOffset: undefined, requestedLimit: undefined });
expect(created.emit).toHaveBeenCalledTimes(1);
expect(created.emit).toHaveBeenCalledWith("reload", undefined, "", undefined, undefined, 100, 0);
});
it("keeps the custom save path from reloading after a pure update", async () => {
const customSave = vi.fn().mockResolvedValue(undefined);
const prepareFullReload = vi.fn();
const { editor, emit } = createSaveTestEditor({ customSaveHandler: { save: customSave }, prepareFullReload });
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(customSave).toHaveBeenCalledTimes(1);
expect(prepareFullReload).not.toHaveBeenCalled();
expect(emit).not.toHaveBeenCalledWith("reload", expect.anything());
});
});
@@ -104,6 +104,7 @@ export interface UseDataGridEditorOptions {
cacheKey?: ComputedRef<string | undefined>;
/** 保存成功后结果负载被原地修改时通知宿主,使缓存的字节估算失效。 */
onResultPayloadMutated?: () => void;
prepareFullReload?: () => void;
emit: {
(event: "reload", sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number): void;
};
@@ -1453,6 +1454,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
}
function reloadCurrentData() {
options.prepareFullReload?.();
options.emit("reload", sql.value, searchText.value, options.currentWhereInput.value, orderByInput.value.trim() || undefined, pageSize.value, (currentPage.value - 1) * pageSize.value);
}
@@ -1490,6 +1492,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
isSaving.value = true;
snapshot.newRowRefs.forEach((row) => savingNewRows.add(row));
const shouldReloadAfterSave = snapshot.newRows.length > 0 || snapshot.deletedRows.size > 0;
// SQL saves may update columns the client can't predict (e.g. an ON UPDATE CURRENT_TIMESTAMP
// column or a trigger), so reload after a pure row update too. Custom (non-SQL) data sources
// keep the original behavior below, unchanged.
const shouldReloadAfterSqlSave = shouldReloadAfterSave || snapshot.dirtyRows.size > 0;
if (customHandler) {
try {
@@ -1609,7 +1615,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
clearSavedPendingChanges(snapshot);
if (!hasPendingChanges.value) exitTransaction();
clearPendingChangeHistory();
if (shouldReloadAfterSave) {
if (shouldReloadAfterSqlSave) {
reloadCurrentData();
}
await finishSaveChanges(snapshot);