feat(grid): temporary page size with optional set-as-default

Refs: #9167
This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-16 21:25:00 +08:00
1 parent 85653427ef
commit b2d56625e0
16 files changed
+193 -20

No files matched your search

@@ -841,6 +841,23 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
const editEditorSettingsBase = ref<EditorSettingsDraft>(editorSettingsDraftFromSettings(settingsStore.editorSettings));
const hasEditorDraftChanges = computed(() => editorSettingsDraftChanged(currentEditorSettingsDraft(), editEditorSettingsBase.value));
// Defaults can also be changed from the result-grid menu while this dialog is
// open. Refresh untouched draft fields so both entry points stay consistent,
// without overwriting an edit the user is actively making here.
watch(
() => [settingsStore.editorSettings.pageSize, settingsStore.editorSettings.tableOpenPageSize] as const,
([pageSize, tableOpenPageSize]) => {
if (editPageSize.value === editEditorSettingsBase.value.pageSize) {
editPageSize.value = pageSize;
editEditorSettingsBase.value.pageSize = pageSize;
}
if (editTableOpenPageSize.value === editEditorSettingsBase.value.tableOpenPageSize) {
editTableOpenPageSize.value = tableOpenPageSize;
editEditorSettingsBase.value.tableOpenPageSize = tableOpenPageSize;
}
},
);
// --- Background image draft state ---
function cloneBackgroundImageDraft(settings: BackgroundImageSettings): BackgroundImageSettings {
return JSON.parse(JSON.stringify(settings)) as BackgroundImageSettings;
+12 -1
View File
@@ -2934,6 +2934,7 @@ watch([localFilterScopeKey, localFilterRestoreKey], ([, restoreKey], [, previous
// --- Pagination ---
const pageSizePreference = computed(() => resolveDataGridPageSizePreference(props.context, props.pageSizePreference));
const defaultPageSize = computed(() => preferredDataGridPageSize(settingsStore.editorSettings, pageSizePreference.value));
const pageSize = ref(preferredDataGridPageSize(settingsStore.editorSettings, pageSizePreference.value, props.pageLimit));
const currentPage = ref(1);
const pageSizeOptions = computed(() => resultPageSizeMenuOptions(pageSize.value));
@@ -3356,7 +3357,6 @@ function checkInfiniteScroll(scroller: HTMLElement) {
function changePageSize(size: number) {
const normalizedSize = normalizeResultPageSize(size);
pageSize.value = normalizedSize;
settingsStore.updateEditorSettings(dataGridPageSizeSettingsPatch(pageSizePreference.value, normalizedSize));
currentPage.value = 1;
lastInfiniteScrollPage = 0;
infiniteScrollAllLoaded = false;
@@ -3365,6 +3365,15 @@ function changePageSize(size: number) {
emit("paginate", 0, normalizedSize, currentWhereInput(), currentOrderBy());
}
function setDefaultPageSize() {
settingsStore.updateEditorSettings(dataGridPageSizeSettingsPatch(pageSizePreference.value, pageSize.value));
}
function applyCustomPageSizeAndSetDefault() {
applyCustomPageSize();
setDefaultPageSize();
}
function applyCustomPageSize() {
changePageSize(normalizeResultPageSize(customPageSizeInput.value, pageSize.value));
}
@@ -13435,6 +13444,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
:infinite-scroll-enabled="infiniteScrollEnabled"
:infinite-scroll-all-loaded="infiniteScrollAllLoaded"
:page-size="pageSize"
:default-page-size="defaultPageSize"
:page-size-menu-items="pageSizeMenuItems"
:export-menu-items="exportMenuItems"
:current-page="currentPage"
@@ -13443,6 +13453,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
:can-jump-last-page="canJumpLastPage"
@select-page-size="selectPageSizeMenuItem"
@apply-custom-page-size="applyCustomPageSize"
@apply-custom-page-size-and-set-default="applyCustomPageSizeAndSetDefault"
@first-page="firstPage"
@previous-page="prevPage"
@next-page="nextPage"
@@ -21,6 +21,7 @@ const props = withDefaults(
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
pageSize: number;
defaultPageSize: number;
pageSizeMenuItems: LightDropdownItem[];
exportMenuItems: LightDropdownItem[];
currentPage: number;
@@ -49,6 +50,7 @@ const emit = defineEmits<{
jumpPage: [page: number];
lastPage: [];
selectExport: [value: string];
applyCustomPageSizeAndSetDefault: [];
}>();
watch(
@@ -103,31 +105,36 @@ function handlePageInputKeydown(event: KeyboardEvent) {
:items="pageSizeMenuItems"
:trigger-label="`${pageSize}${t('grid.rowsPerPageShort')}`"
trigger-class="inline-flex h-5 shrink-0 items-center justify-center whitespace-nowrap rounded-md px-1.5 text-xs hover:bg-accent hover:text-accent-foreground"
content-class="w-36"
content-class="w-52"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="emit('selectPageSize', $event)"
>
<div class="bg-border -mx-1 my-1 h-px" />
<div class="text-muted-foreground px-1.5 py-1 text-xs">{{ t("grid.customRowsPerPage") }}</div>
<div class="flex items-center gap-1 px-1.5 pb-1" @click.stop @keydown.stop>
<div class="px-2 pt-2 text-xs font-medium text-foreground">{{ t("grid.customRowsPerPage") }}</div>
<div class="px-2 pb-2 text-[11px] text-muted-foreground">{{ t("grid.currentAndDefaultPageSize", { current: pageSize, default: defaultPageSize }) }}</div>
<div class="flex items-center gap-1.5 px-2 pb-2" @pointerdown.stop @click.stop @keydown.stop>
<Input
v-model="customPageSizeInput"
type="number"
inputmode="numeric"
:min="MIN_RESULT_PAGE_SIZE"
:max="MAX_RESULT_PAGE_SIZE"
class="h-6 w-20 px-1.5 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
class="h-7 flex-1 px-2 text-sm tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
@keydown.enter.prevent.stop="emit('applyCustomPageSize')"
/>
<Tooltip>
<TooltipTrigger as-child
><Button variant="outline" size="icon" class="h-6 w-6 shrink-0" :aria-label="t('grid.applyPageSize')" @click.stop="emit('applyCustomPageSize')"><Check class="h-3 w-3" /></Button
><Button variant="outline" size="icon" class="h-7 w-7 shrink-0" :aria-label="t('grid.applyForThisQuery')" @click.stop="emit('applyCustomPageSize')"><Check class="h-3.5 w-3.5" /></Button
></TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.applyPageSize") }}</TooltipContent>
</Tooltip>
</div>
<div class="flex gap-1.5 border-t px-2 py-2">
<button type="button" class="flex-1 whitespace-nowrap rounded-md border px-2 py-1.5 text-center text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground" @click.stop="emit('applyCustomPageSize')">{{ t("grid.applyForThisQuery") }}</button>
<button type="button" class="flex-1 whitespace-nowrap rounded-md bg-primary px-2 py-1.5 text-center text-xs text-primary-foreground hover:bg-primary/90" @click.stop="emit('applyCustomPageSizeAndSetDefault')">{{ t("grid.applyAndSetDefault") }}</button>
</div>
</LightDropdown>
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || currentPage <= 1" @click="emit('firstPage')"><ChevronsLeft class="h-3 w-3" /></Button>
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || currentPage <= 1" @click="emit('previousPage')"><ChevronLeft class="h-3 w-3" /></Button>
@@ -65,6 +65,7 @@ function mountGrid(
infiniteScroll?: boolean;
empty?: boolean;
configurePaginationShortcuts?: boolean;
pageSizePreference?: "results" | "table-open";
} = {},
) {
const pinia = createPinia();
@@ -107,6 +108,7 @@ function mountGrid(
result,
databaseType: "mysql",
context: "table-data",
pageSizePreference: options.pageSizePreference,
pageLimit: 100,
pageOffset: options.pageOffset ?? 200,
totalRowCount: options.totalRowCount ?? 500,
@@ -125,7 +127,7 @@ function mountGrid(
app.mount(host);
const mounted = { app, host };
mountedApps.push(mounted);
return { host, paginate };
return { host, paginate, settingsStore };
}
async function settle() {
@@ -146,6 +148,29 @@ function dispatchShortcut(target: HTMLElement, key: string): KeyboardEvent {
return event;
}
function menuActionButton(label: string): HTMLButtonElement {
const button = Array.from(document.querySelectorAll<HTMLButtonElement>('[role="menu"] button')).find((candidate) => candidate.textContent?.trim() === label);
if (!button) throw new Error(`Menu action button not found: ${label}`);
return button;
}
async function openCustomPageSizeMenu(host: HTMLElement): Promise<HTMLInputElement> {
const trigger = Array.from(host.querySelectorAll<HTMLButtonElement>("button[aria-expanded]")).find((button) => button.textContent?.includes("100"));
expect(trigger).toBeDefined();
trigger!.click();
await settle();
const input = document.querySelector<HTMLInputElement>('[role="menu"] input[type="number"]');
expect(input).not.toBeNull();
return input!;
}
async function enterCustomPageSize(host: HTMLElement, size: string): Promise<void> {
const input = await openCustomPageSizeMenu(host);
input.value = size;
input.dispatchEvent(new Event("input", { bubbles: true }));
await settle();
}
afterEach(() => {
for (const { app, host } of mountedApps.splice(0)) {
app.unmount();
@@ -154,6 +179,31 @@ afterEach(() => {
});
describe("DataGrid pagination shortcuts", () => {
it("allows pointer focus and editing in the custom page size input", async () => {
const { host, paginate } = mountGrid();
await settle();
const trigger = Array.from(host.querySelectorAll<HTMLButtonElement>("button[aria-expanded]")).find((button) => button.textContent?.includes("100"));
expect(trigger).toBeDefined();
trigger!.click();
await settle();
const input = document.querySelector<HTMLInputElement>('[role="menu"] input[type="number"]');
expect(input).not.toBeNull();
const pointer = new MouseEvent("pointerdown", { bubbles: true, cancelable: true, button: 0 });
input!.dispatchEvent(pointer);
// Canceling pointerdown suppresses the browser's native focus behavior.
expect(pointer.defaultPrevented).toBe(false);
input!.focus();
input!.value = "250";
input!.dispatchEvent(new Event("input", { bubbles: true }));
await settle();
expect(document.activeElement).toBe(input);
input!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
await settle();
expect(paginate).toHaveBeenCalledOnce();
expect(paginate.mock.calls[0]?.slice(0, 2)).toEqual([0, 250]);
});
it.each(shortcutCases)("runs $functionName through the configured $actionId shortcut", async ({ key, offset }) => {
const { host, paginate } = mountGrid();
await settle();
@@ -277,3 +327,53 @@ describe("DataGrid pagination shortcuts", () => {
expect(actionIds).toHaveLength(4);
});
});
describe("DataGrid custom page size persistence", () => {
it("applying a custom page size is temporary and does not write editor settings", async () => {
const { host, paginate, settingsStore } = mountGrid();
await settle();
const updateEditorSettings = vi.spyOn(settingsStore, "updateEditorSettings");
await enterCustomPageSize(host, "250");
menuActionButton(i18n.global.t("grid.applyForThisQuery")).click();
await settle();
expect(paginate).toHaveBeenCalledOnce();
expect(paginate.mock.calls[0]?.slice(0, 2)).toEqual([0, 250]);
expect(updateEditorSettings).not.toHaveBeenCalled();
expect(settingsStore.editorSettings.pageSize).toBe(100);
expect(settingsStore.editorSettings.tableOpenPageSize).toBe(100);
});
it("set-as-default persists the table-open page size for table grids", async () => {
const { host, paginate, settingsStore } = mountGrid();
await settle();
const updateEditorSettings = vi.spyOn(settingsStore, "updateEditorSettings");
await enterCustomPageSize(host, "250");
menuActionButton(i18n.global.t("grid.applyAndSetDefault")).click();
await settle();
expect(paginate).toHaveBeenCalledOnce();
expect(paginate.mock.calls[0]?.slice(0, 2)).toEqual([0, 250]);
expect(updateEditorSettings).toHaveBeenCalledWith({ tableOpenPageSize: 250 });
expect(settingsStore.editorSettings.tableOpenPageSize).toBe(250);
expect(settingsStore.editorSettings.pageSize).toBe(100);
});
it("set-as-default persists the query page size for results grids", async () => {
const { host, paginate, settingsStore } = mountGrid({ pageSizePreference: "results" });
await settle();
const updateEditorSettings = vi.spyOn(settingsStore, "updateEditorSettings");
await enterCustomPageSize(host, "250");
menuActionButton(i18n.global.t("grid.applyAndSetDefault")).click();
await settle();
expect(paginate).toHaveBeenCalledOnce();
expect(paginate.mock.calls[0]?.slice(0, 2)).toEqual([0, 250]);
expect(updateEditorSettings).toHaveBeenCalledWith({ pageSize: 250 });
expect(settingsStore.editorSettings.pageSize).toBe(250);
expect(settingsStore.editorSettings.tableOpenPageSize).toBe(100);
});
});