mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-22 12:31:57 +02:00
feat(editor): Add an All option to pagination and remove the custom datatable page-size controls (#39850)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e167fd5e09
commit
d7fa8ada34
|
|
@ -0,0 +1,120 @@
|
|||
import type { StoryFn } from '@storybook/vue3-vite';
|
||||
import { computed, ref, type Component } from 'vue';
|
||||
|
||||
import { PAGINATION_ALL_ITEMS_PER_PAGE } from '../N8nPagination';
|
||||
import N8nDataTableServer, { type TableHeader, type TableOptions } from './N8nDataTableServer.vue';
|
||||
|
||||
type Person = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
const roles = ['Admin', 'Member', 'Viewer'];
|
||||
|
||||
const people: Person[] = Array.from({ length: 48 }, (_, index) => {
|
||||
const label = String(index + 1).padStart(2, '0');
|
||||
return {
|
||||
id: label,
|
||||
name: `Person ${label}`,
|
||||
role: roles[index % roles.length],
|
||||
};
|
||||
});
|
||||
|
||||
const headers: Array<TableHeader<Person>> = [
|
||||
{ title: 'Name', key: 'name' },
|
||||
{ title: 'Role', key: 'role' },
|
||||
];
|
||||
|
||||
function sortedPeople(rows: Person[], sortBy: TableOptions['sortBy']) {
|
||||
if (sortBy.length === 0) return rows;
|
||||
|
||||
return rows.slice().sort((a, b) => {
|
||||
for (const sort of sortBy) {
|
||||
const key = sort.id as keyof Person;
|
||||
const diff = String(a[key]).localeCompare(String(b[key]));
|
||||
if (diff !== 0) return sort.desc ? -diff : diff;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function formatSort(sortBy: TableOptions['sortBy']) {
|
||||
if (sortBy.length === 0) return 'none';
|
||||
return sortBy.map((sort) => `${sort.id} ${sort.desc ? 'descending' : 'ascending'}`).join(', ');
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Core/DataTableServer',
|
||||
component: N8nDataTableServer,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Renders one page of rows. The parent supplies that page and the total count. Page, page size, and sort changes emit update:options. Page is 0-indexed.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => ({
|
||||
// The generic table is not assignable to Storybook's component type.
|
||||
components: { N8nDataTableServer: N8nDataTableServer as unknown as Component },
|
||||
setup() {
|
||||
const page = ref(0);
|
||||
const itemsPerPage = ref(10);
|
||||
const sortBy = ref<TableOptions['sortBy']>([]);
|
||||
const latest = ref<TableOptions>({
|
||||
page: page.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
sortBy: [],
|
||||
});
|
||||
const pageItems = computed(() => {
|
||||
const sorted = sortedPeople(people, sortBy.value);
|
||||
if (itemsPerPage.value === PAGINATION_ALL_ITEMS_PER_PAGE) return sorted;
|
||||
|
||||
const start = page.value * itemsPerPage.value;
|
||||
return sorted.slice(start, start + itemsPerPage.value);
|
||||
});
|
||||
const pageSizeLabel = computed(() =>
|
||||
latest.value.itemsPerPage === PAGINATION_ALL_ITEMS_PER_PAGE
|
||||
? 'all'
|
||||
: `${latest.value.itemsPerPage} per page`,
|
||||
);
|
||||
const sortLabel = computed(() => formatSort(latest.value.sortBy));
|
||||
|
||||
function onOptions(payload: TableOptions) {
|
||||
latest.value = payload;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
pageItems,
|
||||
total: people.length,
|
||||
latest,
|
||||
pageSizeLabel,
|
||||
sortLabel,
|
||||
onOptions,
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: var(--spacing--sm); padding: var(--spacing--md);">
|
||||
<N8nDataTableServer
|
||||
v-model:page="page"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="pageItems"
|
||||
:items-length="total"
|
||||
show-all
|
||||
@update:options="onOptions"
|
||||
/>
|
||||
<p style="margin: 0; font-size: var(--font-size--2xs); color: var(--color--text--tint-1);">
|
||||
update:options — page {{ latest.page }} (0-indexed), {{ pageSizeLabel }}, sort {{ sortLabel }}
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, waitFor, within } from '@testing-library/vue';
|
||||
|
||||
import { PAGINATION_ALL_ITEMS_PER_PAGE } from '../N8nPagination';
|
||||
import N8nDataTableServer, { type TableHeader } from './N8nDataTableServer.vue';
|
||||
import { createComponentRenderer } from '../../__tests__/render';
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ const renderComponent = createComponentRenderer(N8nDataTableServer);
|
|||
const getRenderedOptions = async () => {
|
||||
const dropdown = await waitFor(() => screen.getByRole('listbox'));
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
return dropdown.querySelectorAll('.el-select-dropdown__item');
|
||||
return dropdown.querySelectorAll('[data-test-id="select-item"]');
|
||||
};
|
||||
|
||||
const itemFactory = () => ({
|
||||
|
|
@ -119,12 +120,12 @@ describe('N8nDataTableServer', () => {
|
|||
// account for the debounce
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
const option50 = Array.from(options).find((option) => option.textContent === '50');
|
||||
const option50 = Array.from(options).find((option) => option.textContent === '50/page');
|
||||
await userEvent.click(option50!);
|
||||
|
||||
expect(emitted('update:options').length).toBe(4);
|
||||
expect(emitted('update:options').at(-1)).toStrictEqual([
|
||||
expect.objectContaining({ page: 1, itemsPerPage: 50 }),
|
||||
expect.objectContaining({ page: 0, itemsPerPage: 50 }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -184,27 +185,66 @@ describe('N8nDataTableServer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should adjust page to highest available when page size changes and current page exceeds maximum', async () => {
|
||||
it('should render one skeleton row per page size while the first page loads', () => {
|
||||
const { container } = render(N8nDataTableServer, {
|
||||
//@ts-expect-error testing-library errors due to header generics
|
||||
props: { items: [], headers, itemsLength: 100, loading: true, itemsPerPage: 25 },
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll('tbody tr').length).toBe(25);
|
||||
});
|
||||
|
||||
it('should render a bounded skeleton while All is loading', () => {
|
||||
const { container } = render(N8nDataTableServer, {
|
||||
props: {
|
||||
items: [],
|
||||
//@ts-expect-error testing-library errors due to header generics
|
||||
headers,
|
||||
itemsLength: 100,
|
||||
loading: true,
|
||||
itemsPerPage: PAGINATION_ALL_ITEMS_PER_PAGE,
|
||||
showAll: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll('tbody tr').length).toBe(10);
|
||||
});
|
||||
|
||||
it('should return to the first page when the page size changes', 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
|
||||
const selectInput = await findAllByRole('combobox');
|
||||
await userEvent.click(selectInput[0]);
|
||||
|
||||
const options = await getRenderedOptions();
|
||||
expect(options.length).toBe(4);
|
||||
|
||||
const option100 = Array.from(options).find((option) => option.textContent === '100');
|
||||
const option100 = Array.from(options).find((option) => option.textContent === '100/page');
|
||||
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 })]),
|
||||
expect.arrayContaining([expect.objectContaining({ page: 0, itemsPerPage: 100 })]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit all items per page when All is selected', async () => {
|
||||
const { emitted, findAllByRole } = renderComponent({
|
||||
props: { items, headers, itemsLength: 106, showAll: true },
|
||||
});
|
||||
|
||||
const selectInput = await findAllByRole('combobox');
|
||||
await userEvent.click(selectInput[0]);
|
||||
|
||||
const options = await getRenderedOptions();
|
||||
const allOption = Array.from(options).find((option) => option.textContent === 'All');
|
||||
await userEvent.click(allOption!);
|
||||
|
||||
expect(emitted('update:options').at(-1)).toStrictEqual([
|
||||
expect.objectContaining({ itemsPerPage: PAGINATION_ALL_ITEMS_PER_PAGE }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ import type {
|
|||
} from '@tanstack/vue-table';
|
||||
import { createColumnHelper, FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { ElOption, ElSelect, ElSkeletonItem } from 'element-plus';
|
||||
import { ElSkeletonItem } from 'element-plus';
|
||||
|
||||
// `.js` on purpose, unlike the extensionless form used elsewhere in the repo:
|
||||
// `lodash` is CJS with no `exports` map, so Node cannot resolve the extensionless
|
||||
// subpath once a consumer loads our `dist` as native ESM. Bundlers accept both.
|
||||
|
|
@ -47,7 +48,7 @@ import get from 'lodash/get.js';
|
|||
import { computed, h, shallowRef, useSlots, watch } from 'vue';
|
||||
|
||||
import N8nCheckbox from '../../v2/components/Checkbox/Checkbox.vue';
|
||||
import N8nPagination from '../N8nPagination';
|
||||
import N8nPagination, { PAGINATION_ALL_ITEMS_PER_PAGE } from '../N8nPagination';
|
||||
|
||||
type VueClass = string | string[] | Record<string, boolean> | undefined;
|
||||
|
||||
|
|
@ -69,12 +70,25 @@ const props = withDefaults(
|
|||
|
||||
itemSelectable?: boolean | DeepKeys<T> | ((row: T) => boolean);
|
||||
pageSizes?: number[];
|
||||
/**
|
||||
* Render the pager. Set this to false when the screen loads more rows itself.
|
||||
* @default true
|
||||
*/
|
||||
pagination?: boolean;
|
||||
/**
|
||||
* Show an All option in the page size selector.
|
||||
* Selecting it sets items per page to `PAGINATION_ALL_ITEMS_PER_PAGE`.
|
||||
* @default false
|
||||
*/
|
||||
showAll?: boolean;
|
||||
rowProps?: { class?: VueClass } | ((row: T, index: number) => { class?: VueClass });
|
||||
}>(),
|
||||
{
|
||||
itemSelectable: undefined,
|
||||
itemValue: 'id',
|
||||
pageSizes: () => [10, 25, 50, 100],
|
||||
pagination: true,
|
||||
showAll: false,
|
||||
rowProps: undefined,
|
||||
},
|
||||
);
|
||||
|
|
@ -225,22 +239,36 @@ const page = defineModel<number>('page', { default: 0 });
|
|||
watch(page, () => table.setPageIndex(page.value));
|
||||
|
||||
const itemsPerPage = defineModel<number>('items-per-page', { default: 10 });
|
||||
watch(itemsPerPage, () => table.setPageSize(itemsPerPage.value));
|
||||
|
||||
const pagination = computed<PaginationState>({
|
||||
get() {
|
||||
return {
|
||||
pageIndex: page.value,
|
||||
pageSize: itemsPerPage.value,
|
||||
};
|
||||
},
|
||||
set(newValue) {
|
||||
page.value = newValue.pageIndex;
|
||||
itemsPerPage.value = newValue.pageSize;
|
||||
},
|
||||
const ALL_PAGE_SKELETON_ROWS = 10;
|
||||
|
||||
const skeletonRowCount = computed(() =>
|
||||
itemsPerPage.value === PAGINATION_ALL_ITEMS_PER_PAGE
|
||||
? ALL_PAGE_SKELETON_ROWS
|
||||
: itemsPerPage.value,
|
||||
);
|
||||
|
||||
watch(itemsPerPage, (value) => {
|
||||
if (value === PAGINATION_ALL_ITEMS_PER_PAGE) {
|
||||
void emitUpdateOptions({
|
||||
page: page.value,
|
||||
itemsPerPage: value,
|
||||
sortBy: sortBy.value,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
table.setPageSize(value);
|
||||
});
|
||||
|
||||
const showPagination = computed(() => props.itemsLength > Math.min(...props.pageSizes));
|
||||
const paginationState = computed<PaginationState>(() => ({
|
||||
pageIndex: page.value,
|
||||
pageSize: itemsPerPage.value,
|
||||
}));
|
||||
|
||||
const showPagination = computed(
|
||||
() => props.pagination && props.itemsLength > Math.min(...props.pageSizes),
|
||||
);
|
||||
|
||||
const sortBy = defineModel<SortingState>('sort-by', { default: [], required: false });
|
||||
|
||||
|
|
@ -326,15 +354,6 @@ const emitUpdateOptions = useThrottleFn(
|
|||
100,
|
||||
);
|
||||
|
||||
function handlePageSizeChange(newPageSize: number) {
|
||||
// Calculate the maximum available page (0-indexed)
|
||||
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<T>();
|
||||
const table = useVueTable({
|
||||
data,
|
||||
|
|
@ -351,7 +370,7 @@ const table = useVueTable({
|
|||
return sortBy.value;
|
||||
},
|
||||
get pagination() {
|
||||
return pagination.value;
|
||||
return paginationState.value;
|
||||
},
|
||||
get rowSelection() {
|
||||
return rowSelection.value;
|
||||
|
|
@ -361,7 +380,7 @@ const table = useVueTable({
|
|||
onSortingChange: handleSortingChange,
|
||||
onPaginationChange(updaterOrValue) {
|
||||
const newValue =
|
||||
typeof updaterOrValue === 'function' ? updaterOrValue(pagination.value) : updaterOrValue;
|
||||
typeof updaterOrValue === 'function' ? updaterOrValue(paginationState.value) : updaterOrValue;
|
||||
|
||||
// prevent duplicate events from being fired
|
||||
void emitUpdateOptions({
|
||||
|
|
@ -458,7 +477,7 @@ const table = useVueTable({
|
|||
</tr>
|
||||
</template>
|
||||
<template v-if="loading && !table.getRowModel().rows.length">
|
||||
<tr v-for="item in itemsPerPage" :key="item">
|
||||
<tr v-for="item in skeletonRowCount" :key="item">
|
||||
<td
|
||||
v-for="coll in table.getVisibleFlatColumns()"
|
||||
:key="coll.id"
|
||||
|
|
@ -502,22 +521,12 @@ const table = useVueTable({
|
|||
:page="page + 1"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total="itemsLength"
|
||||
:page-sizes="pageSizes"
|
||||
:show-all="showAll"
|
||||
:show-total="false"
|
||||
:show-sizes="false"
|
||||
@update:page="page = $event - 1"
|
||||
@update:items-per-page="itemsPerPage = $event"
|
||||
/>
|
||||
<div class="table-pagination__sizes">
|
||||
<div class="table-pagination__sizes__label">Page size</div>
|
||||
<ElSelect
|
||||
v-model.number="itemsPerPage"
|
||||
class="table-pagination__sizes__select"
|
||||
size="small"
|
||||
:teleported="false"
|
||||
@update:model-value="handlePageSizeChange"
|
||||
>
|
||||
<ElOption v-for="item in pageSizes" :key="item" :label="item" :value="item" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -659,33 +668,10 @@ th.loading-row {
|
|||
}
|
||||
|
||||
.table-pagination {
|
||||
margin-top: 10px;
|
||||
margin-top: var(--spacing--xs);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
&__sizes {
|
||||
display: flex;
|
||||
|
||||
&__label {
|
||||
color: var(--color--text);
|
||||
background-color: var(--color--background--light-2);
|
||||
border: 1px solid var(--color--foreground);
|
||||
border-right: 0;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
&__select {
|
||||
--input--radius--top-left: 0;
|
||||
--input--radius--bottom-left: 0;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { StoryFn } from '@storybook/vue3-vite';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { rows, columns } from './__tests__/data';
|
||||
import N8nDatatable from './Datatable.vue';
|
||||
|
|
@ -10,19 +11,31 @@ export default {
|
|||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'A tabular data component for displaying rows, columns, and table interactions.',
|
||||
component:
|
||||
'A tabular data component for displaying rows, columns, and table interactions. The footer is the shared pagination control. It sits on the right, with a top margin. Page sizes include All.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
export const Default: StoryFn = (args) => ({
|
||||
setup() {
|
||||
const currentPage = ref(typeof args.currentPage === 'number' ? args.currentPage : 1);
|
||||
const rowsPerPage = ref(typeof args.rowsPerPage === 'number' ? args.rowsPerPage : 10);
|
||||
return { args, currentPage, rowsPerPage };
|
||||
},
|
||||
components: {
|
||||
N8nDatatable,
|
||||
},
|
||||
template: '<n8n-datatable v-bind="args"></n8n-datatable>',
|
||||
template: `
|
||||
<n8n-datatable
|
||||
v-bind="args"
|
||||
:current-page="currentPage"
|
||||
:rows-per-page="rowsPerPage"
|
||||
@update:current-page="currentPage = $event"
|
||||
@update:rows-per-page="rowsPerPage = $event"
|
||||
/>
|
||||
`,
|
||||
});
|
||||
|
||||
Default.args = {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, waitFor } from '@testing-library/vue';
|
||||
|
||||
import { PAGINATION_ALL_ITEMS_PER_PAGE } from '../N8nPagination';
|
||||
import { removeDynamicAttributes } from '../../utils';
|
||||
import { rows, columns } from './__tests__/data';
|
||||
import N8nDatatable from './Datatable.vue';
|
||||
|
||||
const stubs = [
|
||||
'N8nOption',
|
||||
'N8nButton',
|
||||
// Ideally we'd like to stub N8nSelect & N8nPagination, but it doesn't work
|
||||
// Ideally we'd like to stub N8nPagination, but it doesn't work
|
||||
// after migrating to setup script:
|
||||
// https://github.com/vuejs/vue-test-utils/issues/2048
|
||||
// 'n8n-select',
|
||||
// 'n8n-pagination',
|
||||
];
|
||||
|
||||
|
|
@ -77,25 +77,67 @@ describe('components', () => {
|
|||
expect(wrapper.container.querySelector('tbody td')?.textContent).toEqual('Row slot');
|
||||
});
|
||||
|
||||
it('should render all rows when rowsPerPage is set to -1', () => {
|
||||
it('should use the default pagination control', () => {
|
||||
const wrapper = render(N8nDatatable, {
|
||||
props: { columns, rows, rowsPerPage: -1 },
|
||||
props: { columns, rows, rowsPerPage },
|
||||
global: { stubs },
|
||||
});
|
||||
|
||||
const pagination = wrapper.container.querySelector('.pagination');
|
||||
expect(pagination?.querySelector('[data-test-id="pagination-list"]')).toBeNull();
|
||||
expect(wrapper.getByTestId('pagination')).toBeInTheDocument();
|
||||
expect(wrapper.getByTestId('pagination-sizes')).toBeInTheDocument();
|
||||
expect(wrapper.container.querySelector('.pageSizeSelector')).toBeNull();
|
||||
});
|
||||
|
||||
const pageSizeSelector = wrapper.container.querySelector('.pageSizeSelector');
|
||||
expect(pageSizeSelector?.textContent).toContain('Page size');
|
||||
it('should render every row when rowsPerPage is All', () => {
|
||||
const wrapper = render(N8nDatatable, {
|
||||
props: { columns, rows, rowsPerPage: PAGINATION_ALL_ITEMS_PER_PAGE },
|
||||
global: { stubs },
|
||||
});
|
||||
|
||||
const allOption = wrapper.getByText('All');
|
||||
expect(allOption).not.toBeNull();
|
||||
expect(wrapper.getByRole('combobox')).toHaveTextContent('All');
|
||||
expect(wrapper.container.querySelectorAll('tbody tr').length).toEqual(rows.length);
|
||||
});
|
||||
|
||||
it('should select All from a later page and show every row', async () => {
|
||||
const currentPage = 2;
|
||||
const wrapper = render(N8nDatatable, {
|
||||
props: { columns, rows, rowsPerPage, currentPage },
|
||||
global: { stubs },
|
||||
});
|
||||
|
||||
expect(wrapper.container.querySelectorAll('tbody tr').length).toEqual(
|
||||
rows.length - rowsPerPage,
|
||||
);
|
||||
|
||||
await userEvent.click(wrapper.getByRole('combobox'));
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(wrapper.getByRole('option', { name: 'All' }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.emitted('update:rowsPerPage')?.[0]).toEqual([PAGINATION_ALL_ITEMS_PER_PAGE]);
|
||||
expect(wrapper.emitted('update:currentPage')?.[0]).toEqual([1]);
|
||||
});
|
||||
|
||||
await wrapper.rerender({
|
||||
columns,
|
||||
rows,
|
||||
rowsPerPage: PAGINATION_ALL_ITEMS_PER_PAGE,
|
||||
currentPage: 1,
|
||||
});
|
||||
|
||||
expect(wrapper.container.querySelectorAll('tbody tr').length).toEqual(rows.length);
|
||||
expect(wrapper.container.querySelectorAll('tbody tr td').length).toEqual(
|
||||
columns.length * rows.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render every row and hide pagination when pagination is disabled', () => {
|
||||
const wrapper = render(N8nDatatable, {
|
||||
props: { columns, rows, pagination: false },
|
||||
global: { stubs },
|
||||
});
|
||||
|
||||
expect(wrapper.queryByTestId('pagination')).not.toBeInTheDocument();
|
||||
expect(wrapper.container.querySelectorAll('tbody tr').length).toEqual(rows.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
<script lang="ts" setup generic="Item extends DatatableRow">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { DatatableProps } from './Datatable.types';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
import type { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types';
|
||||
import { getValueByPath } from '../../utils';
|
||||
import N8nOption from '../N8nOption';
|
||||
import N8nPagination from '../N8nPagination';
|
||||
import N8nSelect from '../N8nSelect';
|
||||
import { N8nPagination, PAGINATION_ALL_ITEMS_PER_PAGE } from '../N8nPagination';
|
||||
import N8nTableBase from '../TableBase';
|
||||
|
||||
const ALL_ROWS = -1;
|
||||
const rowsPerPageOptions = [1, 10, 25, 50, 100];
|
||||
|
||||
defineOptions({ name: 'N8nDatatable' });
|
||||
const props = withDefaults(defineProps<DatatableProps<Item>>(), {
|
||||
|
|
@ -24,19 +21,12 @@ const emit = defineEmits<{
|
|||
'update:rowsPerPage': [value: number];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const rowsPerPageOptions = ref([1, 10, 25, 50, 100]);
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(props.rows.length / props.rowsPerPage);
|
||||
});
|
||||
|
||||
const totalRows = computed(() => {
|
||||
return props.rows.length;
|
||||
});
|
||||
|
||||
const visibleRows = computed(() => {
|
||||
if (props.rowsPerPage === ALL_ROWS) return props.rows;
|
||||
if (!props.pagination || props.rowsPerPage === PAGINATION_ALL_ITEMS_PER_PAGE) return props.rows;
|
||||
|
||||
const start = (props.currentPage - 1) * props.rowsPerPage;
|
||||
const end = start + props.rowsPerPage;
|
||||
|
|
@ -50,16 +40,6 @@ function onUpdateCurrentPage(value: number) {
|
|||
|
||||
function onRowsPerPageChange(value: number) {
|
||||
emit('update:rowsPerPage', value);
|
||||
|
||||
if (value === ALL_ROWS) {
|
||||
onUpdateCurrentPage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPage = Math.ceil(totalRows.value / value);
|
||||
if (maxPage < props.currentPage) {
|
||||
onUpdateCurrentPage(maxPage);
|
||||
}
|
||||
}
|
||||
|
||||
function getTdValue(row: Item, column: DatatableColumn) {
|
||||
|
|
@ -104,35 +84,17 @@ function getThStyle(column: DatatableColumn) {
|
|||
|
||||
<slot name="postdata" />
|
||||
|
||||
<div class="pagination">
|
||||
<N8nPagination
|
||||
v-if="totalPages > 1"
|
||||
:page="currentPage"
|
||||
:items-per-page="rowsPerPage"
|
||||
:total="totalRows"
|
||||
:show-total="false"
|
||||
:show-sizes="false"
|
||||
@update:page="onUpdateCurrentPage"
|
||||
/>
|
||||
|
||||
<div class="pageSizeSelector">
|
||||
<N8nSelect
|
||||
size="mini"
|
||||
:model-value="rowsPerPage"
|
||||
teleported
|
||||
@update:model-value="onRowsPerPageChange"
|
||||
>
|
||||
<template #prepend>{{ t('datatable.pageSize') }}</template>
|
||||
<N8nOption
|
||||
v-for="size in rowsPerPageOptions"
|
||||
:key="size"
|
||||
:label="`${size}`"
|
||||
:value="size"
|
||||
/>
|
||||
<N8nOption :label="`All`" :value="ALL_ROWS"> </N8nOption>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
</div>
|
||||
<N8nPagination
|
||||
v-if="pagination"
|
||||
class="pagination"
|
||||
:page="currentPage"
|
||||
:items-per-page="rowsPerPage"
|
||||
:total="totalRows"
|
||||
:page-sizes="rowsPerPageOptions"
|
||||
show-all
|
||||
@update:page="onUpdateCurrentPage"
|
||||
@update:items-per-page="onRowsPerPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -143,18 +105,8 @@ function getThStyle(column: DatatableColumn) {
|
|||
}
|
||||
|
||||
.pagination {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
bottom: 0;
|
||||
overflow: visible;
|
||||
margin-top: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.pageSizeSelector {
|
||||
text-transform: capitalize;
|
||||
max-width: 150px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -98,41 +98,22 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-c73ff18f="" class="pagination">
|
||||
<div data-v-c73ff18f="" class="n8n-pagination paginationContainer medium" data-test-id="pagination">
|
||||
<!--v-if-->
|
||||
<nav>
|
||||
<div class="paginationList" data-test-id="pagination-list"><span class="disabledTrigger" data-state="closed" data-grace-area-trigger=""><n8n-button-stub icon="chevron-left" variant="ghost" size="medium" loading="false" icononly="true" disabled="true" class="" aria-label="Previous page" type="button" data-test-id="pagination-prev"></n8n-button-stub></span>
|
||||
<!--v-if--><button data-type="page" aria-label="Page 1" aria-current="page" data-selected="true" type="button" class="paginationItem" data-test-id="pagination-item" value="1">1</button><button data-type="page" aria-label="Page 2" type="button" class="paginationItem" data-test-id="pagination-item" value="2">2</button><span class="" data-state="closed" data-grace-area-trigger=""><n8n-button-stub icon="chevron-right" variant="ghost" size="medium" loading="false" icononly="true" disabled="false" class="" aria-label="Next page" type="button" data-test-id="pagination-next"></n8n-button-stub></span>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</nav>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<div data-v-c73ff18f="" class="pageSizeSelector">
|
||||
<div data-v-c73ff18f="" class="n8n-select container withPrepend">
|
||||
<div class="prepend">Page size</div>
|
||||
<div class="el-select el-select--small" popperappendtobody="false" limitpopperwidth="false">
|
||||
<div class="select-trigger el-tooltip__trigger el-tooltip__trigger">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div class="el-input el-input--small el-input--suffix">
|
||||
<!-- input -->
|
||||
<!-- prepend slot -->
|
||||
<!--v-if-->
|
||||
<div class="el-input__wrapper">
|
||||
<!-- prefix slot -->
|
||||
<!--v-if--><input class="el-input__inner" role="combobox" aria-activedescendant="" aria-expanded="false" aria-autocomplete="none" aria-haspopup="listbox" type="text" readonly="" autocomplete="off" tabindex="0" placeholder="Select"><!-- suffix slot --><span class="el-input__suffix"><span class="el-input__suffix-inner"><i class="el-icon el-select__caret el-select__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></i><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--></span></span>
|
||||
</div><!-- append slot -->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
</div>
|
||||
<div data-v-c73ff18f="" class="n8n-pagination paginationContainer medium pagination" data-test-id="pagination">
|
||||
<div class="total" data-test-id="pagination-total">Total 15</div>
|
||||
<nav>
|
||||
<div class="paginationList" data-test-id="pagination-list"><span class="disabledTrigger" data-state="closed" data-grace-area-trigger=""><n8n-button-stub icon="chevron-left" variant="ghost" size="medium" loading="false" icononly="true" disabled="true" class="" aria-label="Previous page" type="button" data-test-id="pagination-prev"></n8n-button-stub></span>
|
||||
<!--v-if--><button data-type="page" aria-label="Page 1" aria-current="page" data-selected="true" type="button" class="paginationItem" data-test-id="pagination-item" value="1">1</button><button data-type="page" aria-label="Page 2" type="button" class="paginationItem" data-test-id="pagination-item" value="2">2</button><span class="" data-state="closed" data-grace-area-trigger=""><n8n-button-stub icon="chevron-right" variant="ghost" size="medium" loading="false" icononly="true" disabled="false" class="" aria-label="Next page" type="button" data-test-id="pagination-next"></n8n-button-stub></span>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</nav>
|
||||
<div data-test-id="pagination-sizes" aria-label="Page size" tabindex="0" class="selectTrigger medium pageSizes" role="combobox" aria-expanded="false" aria-required="false" aria-autocomplete="none" disabled="false" dir="ltr" data-state="closed">
|
||||
<!--v-if--><span style="pointer-events: none;" data-placeholder="Select an option" class="selectValue">10/page</span>
|
||||
<!--v-if--><svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" class="n8n-icon trailingIcon" aria-hidden="true" focusable="false" role="img" height="1em" width="1em" data-icon="chevron-down">
|
||||
<path d="M15.56 5.25 8 12.81.44 5.25 1.5 4.19l6.5 6.5 6.5-6.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!--v-if--><select aria-hidden="true" tabindex="-1" style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(50%); white-space: nowrap; word-wrap: normal;" value="10"></select>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export default {
|
|||
control: 'object',
|
||||
description: 'Options for the page size selector',
|
||||
},
|
||||
showAll: {
|
||||
control: 'boolean',
|
||||
description:
|
||||
'Show an All option in the page size selector. Selecting it emits PAGINATION_ALL_ITEMS_PER_PAGE (-1) and shows every item on one page.',
|
||||
},
|
||||
showTotal: {
|
||||
control: 'boolean',
|
||||
description: 'Show the total item count',
|
||||
|
|
@ -115,6 +120,28 @@ export const WithPageSizes: Story = {
|
|||
total: 500,
|
||||
itemsPerPage: 20,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
showAll: false,
|
||||
showTotal: false,
|
||||
showSizes: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAll: Story = {
|
||||
name: 'With Page Sizes (All)',
|
||||
render: Template,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Set showAll to add an All option. Selecting it emits PAGINATION_ALL_ITEMS_PER_PAGE (-1) and shows every item on one page.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
total: 100,
|
||||
itemsPerPage: 10,
|
||||
pageSizes: [10, 25, 50, 100],
|
||||
showAll: true,
|
||||
showTotal: false,
|
||||
showSizes: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { render, waitFor } from '@testing-library/vue';
|
||||
|
||||
import Pagination from './Pagination.vue';
|
||||
import { PAGINATION_ALL_ITEMS_PER_PAGE } from './Pagination.types';
|
||||
|
||||
describe('components/N8nPagination', () => {
|
||||
describe('rendering', () => {
|
||||
|
|
@ -276,6 +277,29 @@ describe('components/N8nPagination', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should offer All when showAll is set', async () => {
|
||||
const wrapper = render(Pagination, {
|
||||
props: {
|
||||
page: 1,
|
||||
itemsPerPage: 10,
|
||||
total: 100,
|
||||
showAll: true,
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(wrapper.getByRole('combobox'));
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(wrapper.getByRole('option', { name: 'All' }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.emitted('update:itemsPerPage')?.[0]).toEqual([
|
||||
PAGINATION_ALL_ITEMS_PER_PAGE,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit update:itemsPerPage when page size changes', async () => {
|
||||
const wrapper = render(Pagination, {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import type { PaginationRootEmits, PaginationRootProps } from './reka-ui';
|
|||
|
||||
export type PaginationSizes = 'small' | 'medium';
|
||||
|
||||
/**
|
||||
* Items-per-page value for the All option.
|
||||
* Selecting All emits this value and shows every item on one page.
|
||||
*/
|
||||
export const PAGINATION_ALL_ITEMS_PER_PAGE = -1;
|
||||
|
||||
export type PaginationProps = Omit<
|
||||
PaginationRootProps,
|
||||
'dir' | 'asChild' | 'as' | 'itemsPerPage' | 'total'
|
||||
|
|
@ -20,6 +26,12 @@ export type PaginationProps = Omit<
|
|||
* @defaultValue [10, 20, 30, 40, 50, 100]
|
||||
*/
|
||||
pageSizes?: number[];
|
||||
/**
|
||||
* Show an All option in the page size selector.
|
||||
* Selecting it emits `PAGINATION_ALL_ITEMS_PER_PAGE`.
|
||||
* @defaultValue false
|
||||
*/
|
||||
showAll?: boolean;
|
||||
/**
|
||||
* Show the total item count.
|
||||
* @defaultValue true
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import { useI18n } from '@n8n/design-system/composables/useI18n';
|
|||
import type { SelectValue } from '@n8n/design-system/v2/components/Select/Select.types';
|
||||
import N8nSelect from '@n8n/design-system/v2/components/Select/Select.vue';
|
||||
|
||||
import type { PaginationEmits, PaginationProps, PaginationSlots } from './Pagination.types';
|
||||
import {
|
||||
PAGINATION_ALL_ITEMS_PER_PAGE,
|
||||
type PaginationEmits,
|
||||
type PaginationProps,
|
||||
type PaginationSlots,
|
||||
} from './Pagination.types';
|
||||
|
||||
import {
|
||||
PaginationRoot,
|
||||
PaginationList,
|
||||
|
|
@ -30,6 +36,7 @@ const $style = useCssModule();
|
|||
const props = withDefaults(defineProps<PaginationProps>(), {
|
||||
size: 'medium',
|
||||
pageSizes: () => [10, 20, 30, 40, 50, 100],
|
||||
showAll: false,
|
||||
showTotal: true,
|
||||
showSizes: true,
|
||||
showJumper: false,
|
||||
|
|
@ -103,11 +110,20 @@ watch(
|
|||
{ immediate: true },
|
||||
);
|
||||
|
||||
function isAllPageSize() {
|
||||
return currentItemsPerPage.value === PAGINATION_ALL_ITEMS_PER_PAGE;
|
||||
}
|
||||
|
||||
function resolvedPageCount() {
|
||||
if (!props.total || !currentItemsPerPage.value) return 1;
|
||||
if (isAllPageSize() || !props.total || !currentItemsPerPage.value) return 1;
|
||||
return Math.ceil(props.total / currentItemsPerPage.value);
|
||||
}
|
||||
|
||||
function rootItemsPerPage() {
|
||||
if (isAllPageSize()) return Math.max(props.total, 1);
|
||||
return currentItemsPerPage.value;
|
||||
}
|
||||
|
||||
function shouldHide() {
|
||||
return props.hideOnSinglePage && resolvedPageCount() <= 1;
|
||||
}
|
||||
|
|
@ -121,10 +137,16 @@ function isNextDisabled(page: number, pageCount: number) {
|
|||
}
|
||||
|
||||
function pageSizeItems() {
|
||||
return props.pageSizes.map((s) => ({
|
||||
const items = props.pageSizes.map((s) => ({
|
||||
value: String(s),
|
||||
label: t('pagination.pageSizeOption', { size: s }),
|
||||
}));
|
||||
|
||||
if (props.showAll) {
|
||||
items.push({ value: String(PAGINATION_ALL_ITEMS_PER_PAGE), label: t('pagination.all') });
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function handlePageUpdate(newPage: number) {
|
||||
|
|
@ -245,7 +267,7 @@ function handlePagerKeydown(event: KeyboardEvent) {
|
|||
v-slot="{ page: rootPage, pageCount: rootPageCount }"
|
||||
v-bind="rootProps"
|
||||
:page="currentPage"
|
||||
:items-per-page="currentItemsPerPage"
|
||||
:items-per-page="rootItemsPerPage()"
|
||||
:total="total"
|
||||
@update:page="handlePageUpdate"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ Extends Reka UI [`PaginationRootProps`](https://reka-ui.com/docs/components/pagi
|
|||
|
||||
- `defaultItemsPerPage?: number` - Initial items-per-page in uncontrolled mode. Default: `10`
|
||||
- `pageSizes?: number[]` - Options for the page size selector. Default: `[10, 20, 30, 40, 50, 100]`
|
||||
- `showAll?: boolean` - Show an All option in the page size selector. Selecting it emits `PAGINATION_ALL_ITEMS_PER_PAGE` (`-1`) and shows every item on one page. Default: `false`
|
||||
- `showTotal?: boolean` - Show the total item count. Default: `true`
|
||||
- `showSizes?: boolean` - Show the page size selector. Default: `true`
|
||||
- `showJumper?: boolean` - Show the go-to-page jumper. Default: `false`
|
||||
|
|
@ -36,7 +37,7 @@ Extends Reka UI [`PaginationRootProps`](https://reka-ui.com/docs/components/pagi
|
|||
**Events**
|
||||
|
||||
- `@update:page` - Emitted when the current page changes. Payload: `(value: number) => void`
|
||||
- `@update:items-per-page` - Emitted when the page size changes. Payload: `(value: number) => void`
|
||||
- `@update:items-per-page` - Emitted when the page size changes. Payload: `(value: number) => void`. All emits `PAGINATION_ALL_ITEMS_PER_PAGE` (`-1`).
|
||||
|
||||
**Slots**
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,10 @@ import N8nPagination from './Pagination.vue';
|
|||
|
||||
export default N8nPagination;
|
||||
export { N8nPagination };
|
||||
export type * from './Pagination.types';
|
||||
export type {
|
||||
PaginationEmits,
|
||||
PaginationProps,
|
||||
PaginationSizes,
|
||||
PaginationSlots,
|
||||
} from './Pagination.types';
|
||||
export { PAGINATION_ALL_ITEMS_PER_PAGE } from './Pagination.types';
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export { default as N8nNodeCreatorNode } from './N8nNodeCreatorNode';
|
|||
export { default as N8nNodeIcon } from './N8nNodeIcon';
|
||||
export { default as N8nNotice } from './N8nNotice';
|
||||
export { default as N8nOption } from './N8nOption';
|
||||
export { default as N8nPagination } from './N8nPagination';
|
||||
export { default as N8nPagination, PAGINATION_ALL_ITEMS_PER_PAGE } from './N8nPagination';
|
||||
export { default as N8nSectionHeader } from './N8nSectionHeader';
|
||||
export { default as N8nSelectableList } from './N8nSelectableList';
|
||||
export { default as N8nSelectedItemsInfo } from './N8nSelectedItemsInfo';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ export { default as N8nSelect2Item } from './v2/components/Select/SelectItem.vue
|
|||
export type * from './v2/components/Select/Select.types';
|
||||
export { default as N8nCheckbox } from './v2/components/Checkbox/Checkbox.vue';
|
||||
export type * from './v2/components/Checkbox/Checkbox.types';
|
||||
export type * from './components/N8nPagination/Pagination.types';
|
||||
export type {
|
||||
PaginationEmits,
|
||||
PaginationProps,
|
||||
PaginationSizes,
|
||||
PaginationSlots,
|
||||
} from './components/N8nPagination/Pagination.types';
|
||||
export { default as N8nLoading2 } from './v2/components/Loading/Loading.vue';
|
||||
export type * from './v2/components/Loading/Loading.types';
|
||||
export { default as N8nRadioGroupItem } from './components/N8nRadioGroup/RadioGroupItem.vue';
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@ export default {
|
|||
'tags.showMore': (count: number) => `+${count} more`,
|
||||
'tabs.scrollLeft': 'Show previous tabs',
|
||||
'tabs.scrollRight': 'Show next tabs',
|
||||
'datatable.pageSize': 'Page size',
|
||||
'pagination.previousPage': 'Previous page',
|
||||
'pagination.nextPage': 'Next page',
|
||||
'pagination.total': 'Total {total}',
|
||||
'pagination.pageSize': 'Page size',
|
||||
'pagination.pageSizeOption': '{size}/page',
|
||||
'pagination.all': 'All',
|
||||
'pagination.goTo': 'Go to',
|
||||
'pagination.goToPage': 'Go to page',
|
||||
'selectedItemsInfo.selectedOne': '{count} row selected',
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ const onTableAction = (action: string, settings: ChatProviderSettingsDto) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.tableContainer">
|
||||
<div>
|
||||
<div v-if="props.loading">
|
||||
<N8nLoading :loading="props.loading" variant="h1" :class="$style.header" />
|
||||
<N8nLoading :loading="props.loading" variant="p" :rows="5" :shrink-last="false" />
|
||||
|
|
@ -159,6 +159,7 @@ const onTableAction = (action: string, settings: ChatProviderSettingsDto) => {
|
|||
:headers="tableHeaders"
|
||||
:items="settingItems"
|
||||
:items-length="settingItems.length"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #[`item.provider`]="{ item }">
|
||||
<div :class="$style.providerCell">
|
||||
|
|
@ -210,12 +211,6 @@ const onTableAction = (action: string, settings: ChatProviderSettingsDto) => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tableContainer {
|
||||
:global(.table-pagination) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ onMounted(async () => {
|
|||
/>
|
||||
</header>
|
||||
|
||||
<div :class="$style.usageTableContainer">
|
||||
<div>
|
||||
<div v-if="showUsageSectionSkeleton">
|
||||
<N8nLoading :loading="true" variant="h1" :class="$style.usageTableHeader" />
|
||||
<N8nLoading :loading="true" variant="p" :rows="5" :shrink-last="false" />
|
||||
|
|
@ -226,6 +226,7 @@ onMounted(async () => {
|
|||
:headers="tableHeaders"
|
||||
:items="entries"
|
||||
:items-length="entries.length"
|
||||
:pagination="false"
|
||||
:loading="isLoading && isAppending"
|
||||
:item-value="rowId"
|
||||
:row-props="(row) => (isRowClickable(row) ? { class: $style.clickableRow } : {})"
|
||||
|
|
@ -306,12 +307,6 @@ onMounted(async () => {
|
|||
margin-bottom: var(--spacing--5xs);
|
||||
}
|
||||
|
||||
.usageTableContainer {
|
||||
:global(.table-pagination) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.usageTableHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -254,9 +254,3 @@ function resolveCountRoute(row: RoleRow): RouteLocationRaw {
|
|||
color: var(--color--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.pageSizeSelector) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user