fix(data-grid): preserve table page size on refresh

This commit is contained in:
zipg 2026-07-17 17:13:09 +08:00 committed by GitHub
parent 7165ec8911
commit 994ae28aef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 138 additions and 14 deletions

View File

@ -69,6 +69,7 @@ import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/
import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { uuid } from "@/lib/common/utils";
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
import { compactHeaderColumnType, resolveHeaderColumnType } from "@/lib/dataGrid/dataGridColumnType";
@ -2251,7 +2252,7 @@ watch(
);
// --- Pagination ---
const pageSize = ref(normalizeResultPageSize(settingsStore.editorSettings.pageSize));
const pageSize = ref(normalizeResultPageSize(props.context === "table-data" ? (props.pageLimit ?? tableOpenPageLimit()) : settingsStore.editorSettings.pageSize));
const currentPage = ref(1);
const pageSizeOptions = computed(() => resultPageSizeMenuOptions(pageSize.value));
const customPageSizeInput = ref(String(pageSize.value));
@ -2270,6 +2271,8 @@ watch(pageSize, (value) => {
watch(
() => settingsStore.editorSettings.pageSize,
(value) => {
// Table-data segments keep their own pagination state instead of following SQL result settings.
if (props.context === "table-data") return;
pageSize.value = normalizeResultPageSize(value, pageSize.value);
},
);

View File

@ -4,9 +4,12 @@ import { useDataGridActions } from "@/composables/useDataGridActions";
import type { QueryTab } from "@/types/database";
const mocks = vi.hoisted(() => ({
buildTableSelectSql: vi.fn(),
buildSortedQuerySql: vi.fn(),
executeTabSql: vi.fn(),
getConfig: vi.fn(),
setExecuting: vi.fn(),
updateSql: vi.fn(),
}));
vi.mock("vue-i18n", () => ({
@ -17,6 +20,11 @@ vi.mock("@/lib/backend/api", () => ({
buildSortedQuerySql: mocks.buildSortedQuerySql,
}));
vi.mock("@/lib/table/tableSelectSql", () => ({
buildTableSelectSql: mocks.buildTableSelectSql,
quoteTableDataIdentifier: (_databaseType: string, name: string) => `"${name}"`,
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
getConfig: mocks.getConfig,
@ -26,12 +34,8 @@ vi.mock("@/stores/connectionStore", () => ({
vi.mock("@/stores/queryStore", () => ({
useQueryStore: () => ({
executeTabSql: mocks.executeTabSql,
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { pageSize: 100 },
setExecuting: mocks.setExecuting,
updateSql: mocks.updateSql,
}),
}));
@ -39,13 +43,100 @@ vi.mock("@/composables/useToast", () => ({
useToast: () => ({ toast: vi.fn() }),
}));
function tableDataTab(patch: Partial<QueryTab> = {}): QueryTab {
return {
id: "tab-1",
connectionId: "postgres-1",
database: "app",
title: "users",
sql: "SELECT * FROM public.users",
result: { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
mode: "data",
isDirty: false,
isExecuting: false,
isCancelling: false,
isExplaining: false,
tableMetaUpdatedAt: Date.now(),
tableMeta: {
schema: "public",
tableName: "users",
tableType: "TABLE",
columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
primaryKeys: ["id"],
},
...patch,
} as QueryTab;
}
describe("useDataGridActions", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.getConfig.mockReturnValue({ id: "postgres-1", db_type: "postgres" });
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM public.users LIMIT 100 OFFSET 0");
mocks.buildSortedQuerySql.mockResolvedValue({ ok: true, sql: "SELECT sorted" });
});
it("uses the table-data default when toolbar reload has no saved pagination", async () => {
const tab = tableDataTab();
const actions = useDataGridActions(computed(() => tab));
await actions.onReloadData(tab.sql, "", "", "", undefined, undefined, "refresh");
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
expect.objectContaining({
limit: 100,
offset: 0,
}),
);
expect(mocks.executeTabSql).toHaveBeenCalledWith("tab-1", "SELECT * FROM public.users LIMIT 100 OFFSET 0", expect.objectContaining({ pagination: { limit: 100, offset: 0 } }));
expect(mocks.executeTabSql.mock.calls[0]?.[2]).not.toHaveProperty("preserveTotalRowCountDuringExecution");
});
it("preserves the toolbar page segment and offset for table-data refresh", async () => {
const tab = tableDataTab({
resultPageLimit: 25,
resultPageOffset: 50,
});
mocks.buildTableSelectSql.mockResolvedValueOnce("SELECT * FROM public.users LIMIT 25 OFFSET 50");
const actions = useDataGridActions(computed(() => tab));
await actions.onReloadData(tab.sql, "", "", "", 25, 50, "refresh");
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ limit: 25, offset: 50 }));
expect(mocks.executeTabSql).toHaveBeenCalledWith("tab-1", "SELECT * FROM public.users LIMIT 25 OFFSET 50", expect.objectContaining({ pagination: { limit: 25, offset: 50 } }));
expect(mocks.executeTabSql.mock.calls[0]?.[2]).not.toHaveProperty("preserveTotalRowCountDuringExecution");
});
it("keeps SQL result toolbar reload free of table pagination defaults", async () => {
const tab = {
id: "tab-1",
connectionId: "postgres-1",
database: "app",
title: "Query",
sql: "SELECT 1",
result: { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
mode: "query",
isDirty: false,
isExecuting: false,
isCancelling: false,
isExplaining: false,
} as QueryTab;
const actions = useDataGridActions(computed(() => tab));
await actions.onReloadData(tab.sql, "", "", "", undefined, undefined, "refresh");
expect(mocks.buildTableSelectSql).not.toHaveBeenCalled();
expect(mocks.executeTabSql).toHaveBeenCalledWith(
"tab-1",
"SELECT 1",
expect.objectContaining({
resultBaseSql: "SELECT 1",
resultSortedSql: undefined,
preserveResultDuringExecution: true,
}),
);
});
it("excludes hidden primary keys and remaps the selected column for database sorting", async () => {
const tab = {
id: "tab-1",

View File

@ -2,8 +2,8 @@ import { type ComputedRef } from "vue";
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
import * as api from "@/lib/backend/api";
@ -36,7 +36,6 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const { toast } = useToast();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
function quoteIdent(tab: QueryTab, name: string): string {
const config = connectionStore.getConfig(tab.connectionId);
@ -60,7 +59,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
columns: tableMeta?.columns.map((column) => column.name),
primaryKeys,
includeRowId: useRowId,
limit: options.limit ?? settingsStore.editorSettings.pageSize,
limit: options.limit ?? tab.resultPageLimit ?? tableOpenPageLimit(),
...options,
});
}
@ -120,7 +119,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`;
if (tab.mode === "data" && tableMetaForDataTab(tab)) {
tab.whereInput = whereInput ?? "";
const pageLimit = limit ?? settingsStore.editorSettings.pageSize;
const pageLimit = limit ?? tab.resultPageLimit ?? tableOpenPageLimit();
const pageOffset = offset ?? 0;
console.info("[DBX][reloadData:start]", {
traceId,

View File

@ -28,7 +28,7 @@ vi.mock("@/stores/connectionStore", () => ({
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { pageSize: 100 },
editorSettings: { pageSize: 1000 },
}),
}));
@ -162,6 +162,30 @@ describe("queryStore table data refresh", () => {
expect(store.tabs.find((tab) => tab.id === secondTabId)?.result).toBeUndefined();
});
it("uses the table-open default when a refreshed data tab has no saved pagination", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
store.setTableMeta(tabId, {
schema: "public",
tableName: "users",
tableType: "TABLE",
columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
primaryKeys: ["id"],
});
await expect(store.refreshDataTab(tabId)).resolves.toBe(true);
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
expect.objectContaining({
limit: 100,
offset: 0,
}),
);
expect(mocks.executeMulti).toHaveBeenCalledTimes(1);
expect(store.tabs.find((tab) => tab.id === tabId)?.resultPageLimit).toBe(100);
});
it("rejects a repeated refresh while SQL construction is in progress", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();

View File

@ -1863,7 +1863,6 @@ export const useQueryStore = defineStore("query", () => {
const tableMeta = tableMetaForDataTab(tab);
if (!tableMeta?.tableName) return false;
const settingsStore = useSettingsStore();
const connStore = useConnectionStore();
const conn = connStore.getConfig(tab.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
@ -1871,7 +1870,7 @@ export const useQueryStore = defineStore("query", () => {
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
const orderBy = tab.orderByInput?.trim() || sortOrder;
const limit = tab.resultPageLimit ?? settingsStore.editorSettings.pageSize ?? tableOpenPageLimit();
const limit = tab.resultPageLimit ?? tableOpenPageLimit();
const offset = tab.resultPageOffset ?? 0;
const refreshPreparationId = uuid();

View File

@ -71,6 +71,14 @@ test("DataGrid exposes persistent result toolbar slots", () => {
assert.match(dataGrid, /hasResultToolbarLeadingSlot\.value \|\|[\s\S]*hasResultToolbarActionsSlot\.value/);
});
test("table-data toolbar refresh keeps page size independent from SQL editor settings", () => {
const dataGrid = source(dataGridPath);
assert.match(dataGrid, /props\.context === "table-data" \? \(props\.pageLimit \?\? tableOpenPageLimit\(\)\) : settingsStore\.editorSettings\.pageSize/);
assert.match(dataGrid, /if \(props\.context === "table-data"\) return;[\s\S]*pageSize\.value = normalizeResultPageSize\(value, pageSize\.value\)/);
assert.match(dataGrid, /emit\("reload", props\.sql, searchText\.value, currentWhereInput\(\), currentOrderBy\(\), pageSize\.value, \(currentPage\.value - 1\) \* pageSize\.value, "refresh"\)/);
});
test("standalone result views use the same compact toolbar breakpoint", () => {
const contentArea = source(contentAreaPath);
const dataGrid = source(dataGridPath);