feat(grid): temporary page size with optional set-as-default
Refs: #9167
This commit is contained in:
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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+101
-1
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user