diff --git a/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.test.ts index e0b96c3197..8a38331220 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.test.ts @@ -20,7 +20,7 @@ const itemFactory = () => ({ }); type Item = ReturnType; -const items: Item[] = [...Array(20).keys()].map(itemFactory); +const items: Item[] = [...Array(106).keys()].map(itemFactory); const headers: Array> = [ { title: 'Id', @@ -102,26 +102,30 @@ describe('N8nDataTableServer', () => { await userEvent.click(container.querySelector('thead tr th')!); await userEvent.click(within(getByTestId('pagination')).getByLabelText('page 2')); - // change the page size select option - const selectInput = await findAllByRole('combobox'); // Find the select input - await userEvent.click(selectInput[0]); - - const options = await getRenderedOptions(); - expect(options.length).toBe(4); - - const option50 = Array.from(options).find((option) => option.textContent === '50'); - await userEvent.click(option50!); - - expect(emitted('update:options').length).toBeGreaterThanOrEqual(4); + expect(emitted('update:options').length).toBe(3); expect(emitted('update:options')[0]).toStrictEqual([ expect.objectContaining({ sortBy: [{ id: 'id', desc: false }] }), ]); expect(emitted('update:options')[1]).toStrictEqual([ expect.objectContaining({ sortBy: [{ id: 'id', desc: true }] }), ]); - expect(emitted('update:options')[2]).toStrictEqual([expect.objectContaining({ page: 1 })]); - expect(emitted('update:options')[3]).toStrictEqual([ - expect.objectContaining({ itemsPerPage: 50 }), + + // // change the page size select option + const selectInput = await findAllByRole('combobox'); // Find the select input + await userEvent.click(selectInput[0]); + + const options = await getRenderedOptions(); + expect(options.length).toBe(4); + + // account for the debounce + await new Promise((r) => setTimeout(r, 100)); + + const option50 = Array.from(options).find((option) => option.textContent === '50'); + await userEvent.click(option50!); + + expect(emitted('update:options').length).toBe(4); + expect(emitted('update:options').at(-1)).toStrictEqual([ + expect.objectContaining({ page: 1, itemsPerPage: 50 }), ]); }); @@ -157,4 +161,28 @@ describe('N8nDataTableServer', () => { expect(queryByTestId('pagination')).not.toBeInTheDocument(); }); + + it('should adjust page to highest available when page size changes and current page exceeds maximum', async () => { + const { emitted, findAllByRole } = renderComponent({ + props: { items, headers, itemsLength: 106, itemsPerPage: 50, page: 2 }, + }); + + // change the page size select option + const selectInput = await findAllByRole('combobox'); // Find the select input + await userEvent.click(selectInput[0]); + + const options = await getRenderedOptions(); + expect(options.length).toBe(4); + + const option100 = Array.from(options).find((option) => option.textContent === '100'); + await userEvent.click(option100!); + + // With 106 items and 50 per page, max page should be 2 (0-based index 1) + // Since we were on page 2, we should be adjusted to page 1 + expect(emitted('update:options')).toEqual( + expect.arrayContaining([ + expect.arrayContaining([expect.objectContaining({ page: 1, itemsPerPage: 100 })]), + ]), + ); + }); }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue b/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue index e288f69e2a..13763a8caa 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue @@ -37,6 +37,7 @@ import type { Updater, } from '@tanstack/vue-table'; import { createColumnHelper, FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table'; +import { useThrottleFn } from '@vueuse/core'; import { ElCheckbox, ElOption, ElSelect, ElSkeletonItem } from 'element-plus'; import get from 'lodash/get'; import { computed, h, ref, shallowRef, useSlots, watch } from 'vue'; @@ -204,10 +205,7 @@ const page = defineModel('page', { default: 0 }); watch(page, () => table.setPageIndex(page.value)); const itemsPerPage = defineModel('items-per-page', { default: 10 }); -watch(itemsPerPage, () => { - page.value = 0; - table.setPageSize(itemsPerPage.value); -}); +watch(itemsPerPage, () => table.setPageSize(itemsPerPage.value)); const pagination = computed({ get() { @@ -313,6 +311,20 @@ const rowSelection = ref( }, {}), ); +const emitUpdateOptions = useThrottleFn( + (payload: TableOptions) => emit('update:options', payload), + 100, +); + +function handlePageSizeChange(newPageSize: number) { + // Calculate the maximum available page (0-based indexing) + const maxPage = Math.max(0, Math.ceil(props.itemsLength / newPageSize) - 1); + const newPage = Math.min(page.value, maxPage); + + page.value = newPage; + itemsPerPage.value = newPageSize; +} + const columnHelper = createColumnHelper(); const table = useVueTable({ data, @@ -334,12 +346,13 @@ const table = useVueTable({ getCoreRowModel: getCoreRowModel(), onSortingChange: handleSortingChange, onPaginationChange(updaterOrValue) { - pagination.value = + const newValue = typeof updaterOrValue === 'function' ? updaterOrValue(pagination.value) : updaterOrValue; - emit('update:options', { - page: page.value, - itemsPerPage: itemsPerPage.value, + // prevent duplicate events from being fired + void emitUpdateOptions({ + page: newValue.pageIndex, + itemsPerPage: newValue.pageSize, sortBy: sortBy.value, }); }, @@ -474,6 +487,7 @@ const table = useVueTable({ class="table-pagination__sizes__select" size="small" :teleported="false" + @update:model-value="handlePageSizeChange" > diff --git a/packages/frontend/editor-ui/src/views/SettingsUsersView.vue b/packages/frontend/editor-ui/src/views/SettingsUsersView.vue index 5894ce4b6f..b7031a2046 100644 --- a/packages/frontend/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/frontend/editor-ui/src/views/SettingsUsersView.vue @@ -508,4 +508,8 @@ async function onUpdateMfaEnforced(value: boolean) { align-items: center; flex-shrink: 0; } + +.container { + padding-bottom: 20px; +}