feat(connection): persist result page size
This commit is contained in:
parent
38f9fce779
commit
4ef958a7e5
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 扫描数量",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"\)/);
|
||||
});
|
||||
|
|
@ -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({});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue