feat(connection): persist result page size

This commit is contained in:
t8y2 2026-05-19 10:47:17 +08:00
parent 38f9fce779
commit 4ef958a7e5
8 changed files with 152 additions and 10 deletions

View File

@ -24,6 +24,7 @@ import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { aiTestConnection } from "@/lib/api";
import { eventToShortcut } from "@/lib/keyboardShortcuts";
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize } from "@/lib/paginationPageSize";
import {
SHORTCUT_DEFINITIONS,
findShortcutConflict,
@ -51,6 +52,7 @@ const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
const editFontSize = ref(settingsStore.editorSettings.fontSize);
const editTheme = ref(settingsStore.editorSettings.theme);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editPageSize = ref(settingsStore.editorSettings.pageSize);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
@ -67,6 +69,7 @@ watch(
editFontSize.value = settingsStore.editorSettings.fontSize;
editTheme.value = settingsStore.editorSettings.theme;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editPageSize.value = settingsStore.editorSettings.pageSize;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editAppLayout.value = settingsStore.editorSettings.appLayout;
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
@ -90,6 +93,7 @@ function hasChanges(): boolean {
editFontSize.value !== settingsStore.editorSettings.fontSize ||
editTheme.value !== settingsStore.editorSettings.theme ||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
editPageSize.value !== settingsStore.editorSettings.pageSize ||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
@ -105,6 +109,7 @@ function applySettings() {
fontSize: editFontSize.value,
theme: editTheme.value,
executeMode: editExecuteMode.value,
pageSize: normalizeResultPageSize(editPageSize.value),
wordWrap: editWordWrap.value,
appLayout: editAppLayout.value,
redisScanPageSize: editRedisScanPageSize.value,
@ -119,6 +124,7 @@ function resetDefaults() {
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
@ -130,6 +136,10 @@ function onExecuteModeChange(v: any) {
if (v === "all" || v === "current") editExecuteMode.value = v;
}
function onPageSizeInput(event: Event) {
editPageSize.value = normalizeResultPageSize((event.target as HTMLInputElement).value);
}
function onFontFamilyChange(v: any) {
if (typeof v === "string") editFontFamily.value = v;
}
@ -565,6 +575,28 @@ watch(
</Select>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between gap-3">
<Label for="editor-result-page-size">{{ t("settings.resultPageSize") }}</Label>
<span class="text-xs text-muted-foreground tabular-nums">
{{ t("settings.resultPageSizeOption", { count: editPageSize }) }}
</span>
</div>
<Input
id="editor-result-page-size"
type="number"
inputmode="numeric"
:min="MIN_RESULT_PAGE_SIZE"
:max="MAX_RESULT_PAGE_SIZE"
step="1"
:model-value="editPageSize"
@input="onPageSizeInput"
/>
<p class="text-xs text-muted-foreground">{{ t("settings.resultPageSizeDescription") }}</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="flex items-start justify-between gap-4">
<div class="space-y-1">
<Label for="editor-word-wrap">{{ t("settings.wordWrap") }}</Label>

View File

@ -59,8 +59,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@ -105,6 +108,12 @@ import { isCancelSearchShortcut, isFocusSearchShortcut } from "@/lib/keyboardSho
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter";
import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGridSaveUi";
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition } from "@/lib/dataGridColumnFilter";
import {
MAX_RESULT_PAGE_SIZE,
MIN_RESULT_PAGE_SIZE,
normalizeResultPageSize,
resultPageSizeMenuOptions,
} from "@/lib/paginationPageSize";
import {
filterColumnVisibilityOptions,
nextHiddenColumnIndexes,
@ -1114,14 +1123,20 @@ watch(
);
// --- Pagination ---
const pageSize = ref(settingsStore.editorSettings.pageSize);
const pageSize = ref(normalizeResultPageSize(settingsStore.editorSettings.pageSize));
const currentPage = ref(1);
const pageSizeOptions = computed(() => resultPageSizeMenuOptions(pageSize.value));
const customPageSizeInput = ref(String(pageSize.value));
watch(pageSize, (value) => {
customPageSizeInput.value = String(value);
});
watch(
() => [props.pageOffset, props.pageLimit],
([offset, limit]) => {
if (typeof offset !== "number" || typeof limit !== "number" || limit <= 0) return;
pageSize.value = limit;
currentPage.value = Math.floor(offset / limit) + 1;
const normalizedLimit = normalizeResultPageSize(limit);
pageSize.value = normalizedLimit;
currentPage.value = Math.floor(offset / normalizedLimit) + 1;
},
);
const canGoNextPage = computed(() => props.result.has_more === true || props.result.rows.length >= pageSize.value);
@ -1216,11 +1231,16 @@ function nextPage() {
emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy());
}
function changePageSize(size: number) {
pageSize.value = size;
settingsStore.updateEditorSettings({ pageSize: size });
const normalizedSize = normalizeResultPageSize(size);
pageSize.value = normalizedSize;
settingsStore.updateEditorSettings({ pageSize: normalizedSize });
currentPage.value = 1;
resetGridVerticalScroll(true);
emit("paginate", 0, size, currentWhereInput(), currentOrderBy());
emit("paginate", 0, normalizedSize, currentWhereInput(), currentOrderBy());
}
function applyCustomPageSize() {
changePageSize(normalizeResultPageSize(customPageSizeInput.value, pageSize.value));
}
async function lastPage() {
@ -3946,10 +3966,27 @@ defineExpose({
{{ pageSize }}{{ t("grid.rowsPerPageShort") }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-for="s in [50, 100, 500, 1000]" :key="s" @click="changePageSize(s)">
<DropdownMenuContent align="end" class="w-52">
<DropdownMenuItem v-for="s in pageSizeOptions" :key="s" @click="changePageSize(s)">
{{ s }} {{ t("grid.rowsPerPageShort") }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel class="text-xs">{{ t("grid.customRowsPerPage") }}</DropdownMenuLabel>
<div class="flex items-center gap-1.5 px-2 pb-2" @click.stop @keydown.stop>
<Input
v-model="customPageSizeInput"
type="number"
inputmode="numeric"
:min="MIN_RESULT_PAGE_SIZE"
:max="MAX_RESULT_PAGE_SIZE"
class="h-7 text-xs"
@keydown.enter.prevent.stop="applyCustomPageSize"
/>
<Button variant="outline" size="sm" class="h-7 px-2 text-xs" @click.stop="applyCustomPageSize">
<Check class="h-3 w-3" />
{{ t("grid.applyPageSize") }}
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="currentPage <= 1" @click="firstPage">

View File

@ -322,6 +322,8 @@ export default {
filterChangedRows: "Changed",
page: "Page {page}",
rowsPerPage: "Rows per page",
customRowsPerPage: "Custom rows",
applyPageSize: "Apply",
save: "Save",
discard: "Discard",
dismiss: "Dismiss",
@ -1137,6 +1139,10 @@ export default {
executeMode: "Execute Mode (Cmd+Enter)",
executeModeAll: "Execute all SQL",
executeModeCurrent: "Execute statement at cursor",
resultPageSize: "Query result rows per page",
resultPageSizeDescription:
"Used for new queries, table browsing, and result pagination. Very large values may slow queries and rendering.",
resultPageSizeOption: "{count} rows/page",
wordWrap: "Word wrap",
wordWrapDescription: "Wrap long SQL lines within the editor width",
redisScanPageSize: "Redis scan count",

View File

@ -318,6 +318,8 @@ export default {
filterChangedRows: "变更项",
page: "第 {page} 页",
rowsPerPage: "每页行数",
customRowsPerPage: "自定义行数",
applyPageSize: "应用",
save: "保存",
discard: "放弃",
dismiss: "关闭",
@ -1114,6 +1116,9 @@ export default {
executeMode: "执行模式 (Cmd+Enter)",
executeModeAll: "执行全部 SQL",
executeModeCurrent: "执行光标所在语句",
resultPageSize: "查询结果每页行数",
resultPageSizeDescription: "用于新查询、表数据浏览和分页跳转。设置过大可能会降低查询和渲染速度。",
resultPageSizeOption: "{count} 行/页",
wordWrap: "自动换行",
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
redisScanPageSize: "Redis 扫描数量",

View File

@ -0,0 +1,20 @@
export const RESULT_PAGE_SIZE_OPTIONS = [50, 100, 500, 1000];
export const DEFAULT_RESULT_PAGE_SIZE = 100;
export const MIN_RESULT_PAGE_SIZE = 1;
export const MAX_RESULT_PAGE_SIZE = 100000;
export function normalizeResultPageSize(value: unknown, fallback = DEFAULT_RESULT_PAGE_SIZE): number {
const fallbackValue =
Number.isFinite(fallback) && fallback >= MIN_RESULT_PAGE_SIZE
? Math.min(Math.floor(fallback), MAX_RESULT_PAGE_SIZE)
: DEFAULT_RESULT_PAGE_SIZE;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallbackValue;
const rounded = Math.floor(parsed);
if (rounded < MIN_RESULT_PAGE_SIZE) return fallbackValue;
return Math.min(rounded, MAX_RESULT_PAGE_SIZE);
}
export function resultPageSizeMenuOptions(current: number): number[] {
return [...new Set([...RESULT_PAGE_SIZE_OPTIONS, normalizeResultPageSize(current)])].sort((a, b) => a - b);
}

View File

@ -8,6 +8,7 @@ import {
type CustomColumnFormatterConfig,
} from "@/lib/columnFormatter";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/shortcutRegistry";
import { normalizeResultPageSize } from "@/lib/paginationPageSize";
import type { SidebarActivation } from "@/lib/treeNodeClick";
export type AiProvider =
@ -227,7 +228,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>): Edit
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: settings.pageSize ?? DEFAULT_EDITOR_SETTINGS.pageSize,
pageSize: normalizeResultPageSize(settings.pageSize),
redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize,
shortcuts: normalizeShortcutSettings(settings.shortcuts),
sidebarActivation:
@ -312,7 +313,11 @@ export const useSettingsStore = defineStore("settings", () => {
}
function updateEditorSettings(partial: Partial<EditorSettings>) {
Object.assign(editorSettings.value, partial);
const normalizedPartial = {
...partial,
...(partial.pageSize !== undefined ? { pageSize: normalizeResultPageSize(partial.pageSize) } : {}),
};
Object.assign(editorSettings.value, normalizedPartial);
saveEditorSettings(editorSettings.value);
}

View File

@ -0,0 +1,30 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
RESULT_PAGE_SIZE_OPTIONS,
normalizeResultPageSize,
resultPageSizeMenuOptions,
} from "../../apps/desktop/src/lib/paginationPageSize.ts";
import { readFileSync } from "node:fs";
test("normalizes query result page sizes into a safe range", () => {
assert.equal(normalizeResultPageSize(undefined), 100);
assert.equal(normalizeResultPageSize(0), 100);
assert.equal(normalizeResultPageSize(-5), 100);
assert.equal(normalizeResultPageSize(42.8), 42);
assert.equal(normalizeResultPageSize(200000), 100000);
});
test("query result page size menu includes the current custom value", () => {
assert.deepEqual(RESULT_PAGE_SIZE_OPTIONS, [50, 100, 500, 1000]);
assert.deepEqual(resultPageSizeMenuOptions(5000), [50, 100, 500, 1000, 5000]);
assert.deepEqual(resultPageSizeMenuOptions(100), [50, 100, 500, 1000]);
});
test("data grid page size menu exposes a custom input", () => {
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
assert.match(source, /customPageSizeInput/);
assert.match(source, /settingsStore\.updateEditorSettings\(\{ pageSize: normalizedSize \}\)/);
assert.match(source, /t\("grid\.customRowsPerPage"\)/);
});

View File

@ -16,6 +16,13 @@ test("keeps a saved Redis scan page size", () => {
assert.equal(normalizeEditorSettings({ redisScanPageSize: 5000 }).redisScanPageSize, 5000);
});
test("normalizes saved query result page size", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.pageSize, 100);
assert.equal(normalizeEditorSettings({ pageSize: 5000 }).pageSize, 5000);
assert.equal(normalizeEditorSettings({ pageSize: 200000 }).pageSize, 100000);
assert.equal(normalizeEditorSettings({ pageSize: 0 }).pageSize, 100);
});
test("defaults shortcut settings", () => {
const settings = normalizeEditorSettings({});