From 84c51b1bd96b2a66cfb4953b349df73ce65cf29c Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Fri, 13 Jun 2025 10:08:08 +0200 Subject: [PATCH] fix(editor): Fix pagination and sorting issue for insights (#16288) --- .../N8nDataTableServer.test.ts | 49 +++++++++++++++++-- .../N8nDataTableServer/N8nDataTableServer.vue | 12 +++-- .../insights/components/InsightsDashboard.vue | 2 +- 3 files changed, 55 insertions(+), 8 deletions(-) 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 a1f8481028..305a631192 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 @@ -1,8 +1,22 @@ import userEvent from '@testing-library/user-event'; -import { render, within } from '@testing-library/vue'; +import { render, screen, waitFor, within } from '@testing-library/vue'; +import { config } from '@vue/test-utils'; +import ElementPlus from 'element-plus'; + +import { createComponentRenderer } from '@n8n/design-system/__tests__/render'; import N8nDataTableServer, { type TableHeader } from './N8nDataTableServer.vue'; +const renderComponent = createComponentRenderer(N8nDataTableServer); + +const getRenderedOptions = async () => { + const dropdown = await waitFor(() => screen.getByRole('listbox')); + expect(dropdown).toBeInTheDocument(); + return dropdown.querySelectorAll('.el-select-dropdown__item'); +}; + +config.global.plugins.push(ElementPlus); + const itemFactory = () => ({ id: crypto.randomUUID() as string, firstName: crypto.randomUUID() as string, @@ -84,8 +98,7 @@ describe('N8nDataTableServer', () => { }); it('should emit options for sorting / pagination', async () => { - const { container, emitted, getByTestId } = render(N8nDataTableServer, { - //@ts-expect-error testing-library errors due to header generics + const { container, emitted, getByTestId, findAllByRole } = renderComponent({ props: { items, headers, itemsLength: 100 }, }); @@ -93,7 +106,17 @@ describe('N8nDataTableServer', () => { await userEvent.click(container.querySelector('thead tr th')!); await userEvent.click(within(getByTestId('pagination')).getByLabelText('page 2')); - expect(emitted('update:options').length).toBe(3); + // 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')[0]).toStrictEqual([ expect.objectContaining({ sortBy: [{ id: 'id', desc: false }] }), ]); @@ -101,6 +124,24 @@ describe('N8nDataTableServer', () => { 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 }), + ]); + }); + + it('should emit options for sorting with initial sort', async () => { + const { container, emitted } = renderComponent({ + props: { items, headers, itemsLength: 100, sortBy: [{ id: 'id', desc: true }] }, + }); + + await userEvent.click(container.querySelector('thead tr th')!); + await userEvent.click(container.querySelector('thead tr th')!); + + expect(emitted('update:options').length).toBe(2); + expect(emitted('update:options')[0]).toStrictEqual([expect.objectContaining({ sortBy: [] })]); + expect(emitted('update:options')[1]).toStrictEqual([ + expect.objectContaining({ sortBy: [{ id: 'id', desc: false }] }), + ]); }); it('should not show the pagination if there are no items', async () => { 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 2d58c70185..92507fac8f 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nDataTableServer/N8nDataTableServer.vue @@ -205,7 +205,10 @@ 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)); +watch(itemsPerPage, () => { + page.value = 0; + table.setPageSize(itemsPerPage.value); +}); const pagination = computed({ get() { @@ -225,13 +228,16 @@ const showPagination = computed(() => props.itemsLength > Math.min(...props.page const sortBy = defineModel('sort-by', { default: [], required: false }); function handleSortingChange(updaterOrValue: Updater) { - sortBy.value = + const newValue = typeof updaterOrValue === 'function' ? updaterOrValue(sortBy.value) : updaterOrValue; + sortBy.value = newValue; + // Use newValue instead of sortBy.value to ensure the latest value is used + // This is because of the async nature of the Vue reactivity system emit('update:options', { page: page.value, itemsPerPage: itemsPerPage.value, - sortBy: sortBy.value, + sortBy: newValue, }); } diff --git a/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue index 6c2f0b9395..b18526bcc8 100644 --- a/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue +++ b/packages/frontend/editor-ui/src/features/insights/components/InsightsDashboard.vue @@ -57,7 +57,7 @@ const transformFilter = ({ id, desc }: { id: string; desc: boolean }) => { const fetchPaginatedTableData = ({ page = 0, - itemsPerPage = 20, + itemsPerPage = 25, sortBy, dateRange = selectedDateRange.value, }: {