fix(data-grid): preserve table page size on refresh
This commit is contained in:
parent
7165ec8911
commit
994ae28aef
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue