diff --git a/changelog/_13794.txt b/changelog/_13794.txt new file mode 100644 index 0000000000..dfd8987864 --- /dev/null +++ b/changelog/_13794.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix secrets table pagination when switching page sizes. +``` \ No newline at end of file diff --git a/ui/lib/core/addon/components/list-table.ts b/ui/lib/core/addon/components/list-table.ts index 208aef4459..3369ff952e 100644 --- a/ui/lib/core/addon/components/list-table.ts +++ b/ui/lib/core/addon/components/list-table.ts @@ -73,7 +73,10 @@ export default class ListTable extends Component { } @action - handlePaginationChange(action: 'currentPage' | 'pageSize', value: number) { + async handlePaginationChange(action: 'currentPage' | 'pageSize', value: number) { + if (action === 'pageSize') { + await this.resetPagination(); + } this[action] = value; } @@ -88,9 +91,9 @@ export default class ListTable extends Component { } // TEMPLATE HELPERS - isObject = (value: any) => typeof value === 'object' && value !== null; + isObject = (value: unknown) => typeof value === 'object' && value !== null; - identifier = (cellData: Record) => { + identifier = (cellData: Record) => { const firstColumn = this.args.columns[0]?.key; // Use selectionKeyField if provided, otherwise default to value of the first column const identifier = this.args.selectionKeyField || firstColumn; diff --git a/ui/lib/core/addon/utils/paginate-list.ts b/ui/lib/core/addon/utils/paginate-list.ts index 808bac274a..20f6e472a5 100644 --- a/ui/lib/core/addon/utils/paginate-list.ts +++ b/ui/lib/core/addon/utils/paginate-list.ts @@ -34,31 +34,33 @@ export function paginate(data: T[], options: PaginateOptions = {}) { const { page = 1, pageSize = DEFAULT_PAGE_SIZE, filter, filterKey } = options; if (Array.isArray(data)) { - let filteredData = [...data]; - // filter data before paginating if filter is provided - if (filter) { - filteredData = data.filter((item) => { - const filterValue = filterKey ? (item as Record)[filterKey] : item; - if (typeof filterValue === 'string') { - return filterValue.toLowerCase().includes(filter.toLowerCase()); - } - return false; - }); - } + let filteredData = filter + ? data.filter((item) => { + const filterValue = filterKey ? (item as Record)[filterKey] : item; + if (typeof filterValue === 'string') { + return filterValue.toLowerCase().includes(filter.toLowerCase()); + } + return false; + }) + : [...data]; - const lastPage = Math.ceil(filteredData.length / pageSize); - const start = (page - 1) * pageSize; + const filteredTotal = filteredData.length; + const lastPage = Math.ceil(filteredTotal / pageSize); + // Verify that the page number does not go out of bounds, if so adjust to show + // the first page of results + const currentPage = page > lastPage ? 1 : page; + const start = (currentPage - 1) * pageSize; const end = start + pageSize; filteredData = filteredData.slice(start, end); // add meta data previously from lazyPaginatedQuery since components expect it Object.defineProperty(filteredData, 'meta', { value: { - currentPage: page, + currentPage, lastPage, nextPage: page + 1, prevPage: page - 1, total: data.length, - filteredTotal: filteredData.length, + filteredTotal, pageSize, }, writable: false, diff --git a/ui/tests/integration/components/list-table-test.js b/ui/tests/integration/components/list-table-test.js index 9d4ea150f2..90ec5af71e 100644 --- a/ui/tests/integration/components/list-table-test.js +++ b/ui/tests/integration/components/list-table-test.js @@ -177,6 +177,36 @@ module('Integration | Component | list-table', function (hooks) { .hasText('5', 'custom table item renders yielded badge'); }); + test('it shows first page data and correct metadata when navigated page exceeds new data bounds', async function (assert) { + const moreData = [ + { island: 'Tahiti', visit_length: 12, trip_date: '2025-05-10T00:00:00.000Z' }, + { island: 'Barbados', visit_length: 6, trip_date: '2025-08-25T00:00:00.000Z' }, + { island: 'Cyprus', visit_length: 9, trip_date: '2026-03-12T00:00:00.000Z' }, + { island: 'Jamaica', visit_length: 7, trip_date: '2025-11-05T00:00:00.000Z' }, + ]; + this.data = [...MOCK_DATA, ...moreData]; + await this.renderComponent(); + + await fillIn(GENERAL.paginationSizeSelector, '5'); + await click(GENERAL.nextPage); + assert.dom(GENERAL.paginationInfo).hasText(`6–10 of ${this.data.length}`, 'navigated to page 2'); + + // Replace data with fewer items than current page offset such that page 2 no longer exists + this.set('data', [ + { island: 'Palawan', visit_length: 9, trip_date: '2025-11-14T00:00:00.000Z' }, + { island: 'Mykonos', visit_length: 3, trip_date: '2026-02-28T00:00:00.000Z' }, + ]); + + await waitFor(GENERAL.paginationInfo); + assert + .dom(GENERAL.paginationInfo) + .hasText( + `1–2 of ${this.data.length}`, + 'falls back to page 1 when current page exceeds new data bounds' + ); + assert.dom(GENERAL.tableData(0, 'island')).hasText('Palawan', 'first page data is shown after fallback'); + }); + test('it resets pagination when data changes', async function (assert) { const moreData = [ { island: 'Tahiti', visit_length: 12, trip_date: '2025-05-10T00:00:00.000Z' }, diff --git a/ui/tests/unit/utils/paginate-list-test.js b/ui/tests/unit/utils/paginate-list-test.js index 616a574970..2b6ad8ad1c 100644 --- a/ui/tests/unit/utils/paginate-list-test.js +++ b/ui/tests/unit/utils/paginate-list-test.js @@ -59,7 +59,7 @@ module('Unit | Utility | paginate-list', function (hooks) { nextPage: 3, prevPage: 1, total: 20, - filteredTotal: 3, + filteredTotal: 20, pageSize: 3, }; assert.deepEqual(meta, expectedMeta, 'returns correct meta data'); @@ -70,4 +70,37 @@ module('Unit | Utility | paginate-list', function (hooks) { const expected = [18, 19]; assert.deepEqual(paginatedData, expected, 'returns correct items for last page'); }); + + test('filteredTotal reflects total matching items', function (assert) { + // 20 items, filter matches first 6 (0-5), paginate to page 1 with size 4 + const data = Array.from({ length: 20 }, (_, i) => ({ id: i, name: i < 6 ? `match-${i}` : `skip-${i}` })); + const { meta } = paginate(data, { page: 1, pageSize: 4, filter: 'match', filterKey: 'name' }); + assert.strictEqual(meta.filteredTotal, 6, 'filteredTotal is total matching items across all pages'); + assert.strictEqual(meta.lastPage, 2, 'lastPage is based on filteredTotal'); + }); + + test('it should reset to page 1 when page exceeds lastPage', function (assert) { + // 20 items, pageSize 10 = 2 pages; requesting page 5 should fall back to page 1 + const paginatedData = paginate(this.items, { page: 5, pageSize: 10 }); + assert.deepEqual( + paginatedData, + this.items.slice(0, 10), + 'returns first page of items when page is out of bounds' + ); + assert.strictEqual( + paginatedData.meta.currentPage, + 1, + 'currentPage in meta is 1, not the out-of-bounds page' + ); + }); + + test('meta currentPage matches actual data shown when page is out of bounds', function (assert) { + const { meta } = paginate(this.items, { page: 99, pageSize: 5 }); + assert.strictEqual( + meta.currentPage, + 1, + 'currentPage in meta reflects actual page shown, not requested page' + ); + assert.strictEqual(meta.lastPage, 4, 'lastPage is computed correctly'); + }); });