diff --git a/lib/public/app.css b/lib/public/app.css index 9fe5a6a7f1..97516c94b8 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -1073,6 +1073,54 @@ label { } } +.pagination-bar { + font-size: 0.875rem; +} + +/* + * Pagination on phones: everything stays on one line, the buttons to jump to the first and last pages and the page numbers far from + * the current one are removed, the other buttons are compacted to fit and the rows count is shortened to "first-last/total" + */ +.pagination-count-short { + display: none; +} + +@media screen and (max-width: 767px) { + .pagination-count-long { + display: none; + } + + .pagination-count-short { + display: inline; + } + + .pagination-bar > .pagination-count { + gap: 0; + } + + .pagination-bar > .pagination-amount { + width: auto; + white-space: nowrap; + } + + .pagination-bar #pageMoveFirst, + .pagination-bar #pageMoveLast, + .pagination-bar #pageSelector .btn-tab:not(.near-page) { + display: none; + } + + .pagination-bar #pageSelector .btn { + margin: 0 1px; + padding-left: 0.4rem; + padding-right: 0.4rem; + } + + .pagination-bar > .pagination-amount .btn { + padding-left: 0.6em; + padding-right: 0.6em; + } +} + /* xl */ @media screen and (min-width: 1920px) { .columns-3-xl { diff --git a/lib/public/components/Pagination/amountSelector.js b/lib/public/components/Pagination/amountSelector.js index 4d2fbe203a..39d4a7637a 100644 --- a/lib/public/components/Pagination/amountSelector.js +++ b/lib/public/components/Pagination/amountSelector.js @@ -143,7 +143,7 @@ const amountSelector = (paginationModel) => paginationModel.toggleAmountDropdownVisibility(); }, }, - `Rows per page: ${paginationModel.isInfiniteScrollEnabled ? 'Infinite' : paginationModel.itemsPerPage} `, + `Rows: ${paginationModel.isInfiniteScrollEnabled ? 'Infinite' : paginationModel.itemsPerPage} `, iconCaretTop(), ), h('.dropup-menu', mapAmounts(paginationModel)), diff --git a/lib/public/components/Pagination/pageSelector.js b/lib/public/components/Pagination/pageSelector.js index 2bad839a29..1a1ba862db 100644 --- a/lib/public/components/Pagination/pageSelector.js +++ b/lib/public/components/Pagination/pageSelector.js @@ -16,18 +16,30 @@ import { iconArrowThickLeft, iconArrowThickRight } from '/js/src/icons.js'; const MAX_VISIBLE_PAGES = 5; +const NEAR_PAGES_COUNT = 3; + /** * Render the page buttons + * + * The buttons of the pages near the current one are flagged, to be the only ones displayed on small screens + * * @param {Array} pages An array of the page numbers that require buttons * @param {number} currentPage Index of the current page + * @param {number} pagesCount The total amount of pages available * @param {Function} onPageChange Function called when switching to a new page with its index as argument * @return {Array} A collection of vnodes containing the individual page buttons */ -const getPageButtons = (pages, currentPage, onPageChange) => pages.map((page) => h( - `button.btn.btn-tab.mh1${page === currentPage ? ' selected' : ''}#page${page}`, - { onclick: () => onPageChange(page) }, - page, -)); +const getPageButtons = (pages, currentPage, pagesCount, onPageChange) => { + const firstNearPage = Math.max(1, Math.min(currentPage - 1, pagesCount - NEAR_PAGES_COUNT + 1)); + const lastNearPage = firstNearPage + NEAR_PAGES_COUNT - 1; + + return pages.map((page) => { + const selectedClass = page === currentPage ? '.selected' : ''; + const nearPageClass = page >= firstNearPage && page <= lastNearPage ? '.near-page' : ''; + + return h(`button.btn.btn-tab.mh1${selectedClass}${nearPageClass}#page${page}`, { onclick: () => onPageChange(page) }, page); + }); +}; /** * Render the page selector component @@ -42,11 +54,11 @@ const pageSelector = (currentPage, pagesCount, onPageChange) => { let pageButtons; if (pagesCount <= MAX_VISIBLE_PAGES) { - pageButtons = getPageButtons(pages, currentPage, onPageChange); + pageButtons = getPageButtons(pages, currentPage, pagesCount, onPageChange); } else { const frontBound = Math.min(Math.max(0, currentPage - MAX_VISIBLE_PAGES + 2), pagesCount - MAX_VISIBLE_PAGES); const rearBound = Math.min(Math.max(MAX_VISIBLE_PAGES, currentPage + MAX_VISIBLE_PAGES - 3), pagesCount); - pageButtons = getPageButtons(pages.slice(frontBound, rearBound), currentPage, onPageChange); + pageButtons = getPageButtons(pages.slice(frontBound, rearBound), currentPage, pagesCount, onPageChange); } return h( '.btn-group#pageSelector', diff --git a/lib/public/components/Pagination/paginationComponent.js b/lib/public/components/Pagination/paginationComponent.js index 4d3fb857ae..94db5b2dc4 100644 --- a/lib/public/components/Pagination/paginationComponent.js +++ b/lib/public/components/Pagination/paginationComponent.js @@ -68,19 +68,24 @@ export const paginationComponent = (pagination) => { // Trigger scroll once if the component has been loaded in infinite mode and the current scroll need more elements scrollListener(); + // On small screens the message is shortened to "first-last/total" const itemsCountMessage = [ - 'Rows', + h('span.pagination-count-long', 'Rows'), // +1 because users expect indexing to start from 1 h('strong#firstRowIndex', pagination.firstItemOffset + 1), '-', h('strong#lastRowIndex', Math.min(pagination.firstItemOffset + pagination.itemsPerPage, pagination.itemsCount)), ]; if (pagination.itemsCount) { - itemsCountMessage.push('of', h('strong#totalRowsCount', pagination.itemsCount)); + itemsCountMessage.push( + h('span.pagination-count-long', 'of'), + h('span.pagination-count-short', '/'), + h('strong#totalRowsCount', pagination.itemsCount), + ); } return h( - '.flex-row.justify-between.pv3', + '.flex-row.justify-between.pv3.pagination-bar', { /** @@ -118,7 +123,7 @@ export const paginationComponent = (pagination) => { }, }, [ - h('.w-15', amountSelector(pagination)), + h('.w-15.pagination-amount', amountSelector(pagination)), !isInfiniteScrollEnabled && [ pageSelector( currentPage, @@ -127,7 +132,7 @@ export const paginationComponent = (pagination) => { pagination.currentPage = page; }, ), - h('.gray-darker.flex-row.g1', itemsCountMessage), + h('.gray-darker.flex-row.g1.pagination-count', itemsCountMessage), ], ], ); diff --git a/test/public/dataPasses/overviewPerLhcPeriod.test.js b/test/public/dataPasses/overviewPerLhcPeriod.test.js index 949241ba9e..b059399497 100644 --- a/test/public/dataPasses/overviewPerLhcPeriod.test.js +++ b/test/public/dataPasses/overviewPerLhcPeriod.test.js @@ -149,7 +149,7 @@ module.exports = () => { await goToPage(page, 'data-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 2 } }); // Expect the amount selector to currently be set to 9 (because of the defined page height) - await expectInnerText(page, '.dropup button', 'Rows per page: 9 '); + await expectInnerText(page, '.dropup button', 'Rows: 9 '); // Expect the dropdown options to be visible when it is selected await pressElement(page, '.dropup button'); diff --git a/test/public/dataPasses/overviewPerSimulationPass.test.js b/test/public/dataPasses/overviewPerSimulationPass.test.js index 7ad2d7f0dd..33d8845c78 100644 --- a/test/public/dataPasses/overviewPerSimulationPass.test.js +++ b/test/public/dataPasses/overviewPerSimulationPass.test.js @@ -89,7 +89,7 @@ module.exports = () => { await goToPage(page, 'data-passes-per-simulation-pass-overview', { queryParameters: { simulationPassId: 1 } }); // Expect the amount selector to currently be set to 9 (because of the defined page height) - await expectInnerText(page, '.dropup button', 'Rows per page: 9 '); + await expectInnerText(page, '.dropup button', 'Rows: 9 '); // Expect the dropdown options to be visible when it is selected await pressElement(page, '.dropup button'); diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index fccf684d4e..d2c58d927c 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -166,28 +166,34 @@ module.exports = () => { }); it('dynamically switches between visible pages in the page selector', async () => { - // Override the amount of runs visible per page manually - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.envs.overviewModel.pagination.itemsPerPage = 1; - }); - await waitForTableLength(page, 1); - - // Expect the page five button to now be visible, but no more than that - await page.waitForSelector('#page5'); - await page.waitForSelector('#page6', { hidden: true }); - - // Expect the page one button to have fallen away when clicking on page five button - await pressElement(page, '#page5'); - await page.waitForSelector('#page1', { hidden: true }); - - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.envs.overviewModel.pagination.reset(); - // eslint-disable-next-line no-undef - model.envs.overviewModel.pagination.notify(); - }); - }); + // The page selector of phones (screens narrower than 768px) only displays the three page buttons nearest to the current one + const viewport = page.viewport(); + await page.setViewport({ ...viewport, width: 800 }); + try { + // Override the amount of runs visible per page manually + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.envs.overviewModel.pagination.itemsPerPage = 1; + }); + await waitForTableLength(page, 1); + + // Expect the page five button to now be visible, but no more than that + await page.waitForSelector('#page5'); + await page.waitForSelector('#page6', { hidden: true }); + + // Expect the page one button to have fallen away when clicking on page five button + await pressElement(page, '#page5'); + await page.waitForSelector('#page1', { hidden: true }); + + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.envs.overviewModel.pagination.reset(); + // eslint-disable-next-line no-undef + model.envs.overviewModel.pagination.notify(); + }); + } finally { + await page.setViewport(viewport); + } }); it('should successfully display the list of related runs as hyperlinks to their details page', async () => { await waitForNavigation(page, () => pressElement(page, 'a#env-overview', true)); diff --git a/test/public/flps/overview.test.js b/test/public/flps/overview.test.js index 4a741e2ff3..f002ede36f 100644 --- a/test/public/flps/overview.test.js +++ b/test/public/flps/overview.test.js @@ -135,7 +135,7 @@ module.exports = () => { it('can set how many flps are available per page', async () => { // Expect the amount selector to currently be set to Infinite (after the previous test) - await expectInnerText(page, `${amountSelectorSelector} button`, 'Rows per page: Infinite '); + await expectInnerText(page, `${amountSelectorSelector} button`, 'Rows: Infinite '); await pressElement(page, `${amountSelectorSelector} button`); await page.waitForSelector(`${amountSelectorSelector} .dropup-menu`); @@ -146,20 +146,26 @@ module.exports = () => { }); it('dynamically switches between visible pages in the page selector', async () => { - await pressElement(page, amountSelectorButtonSelector); - await fillInput(page, `${amountSelectorSelector} input`, '1', ['input', 'change']); - await waitForTableLength(page, 1); - - // Expect the page five button to now be visible, but no more than that - const pageFiveButton = await page.$('#page5'); - expect(Boolean(pageFiveButton)).to.be.true; - const pageSixButton = await page.$('#page6'); - expect(Boolean(pageSixButton)).to.be.false; - - // Expect the page one button to have fallen away when clicking on page five button - await pressElement(page, '#page5'); - await page.waitForSelector('#page1', { hidden: true }); - }); + // The page selector of phones (screens narrower than 768px) only displays the three page buttons nearest to the current one + const viewport = page.viewport(); + await page.setViewport({ ...viewport, width: 800 }); + try { + await pressElement(page, amountSelectorButtonSelector); + await fillInput(page, `${amountSelectorSelector} input`, '1', ['input', 'change']); + await waitForTableLength(page, 1); + + // Expect the page five button to now be visible, but no more than that + const pageFiveButton = await page.$('#page5'); + expect(Boolean(pageFiveButton)).to.be.true; + const pageSixButton = await page.$('#page6'); + expect(Boolean(pageSixButton)).to.be.false; + + // Expect the page one button to have fallen away when clicking on page five button + await pressElement(page, '#page5'); + await page.waitForSelector('#page1', { hidden: true }); + } finally { + await page.setViewport(viewport); + } }); it('notifies if table loading returned an error', async () => { await pressElement(page, amountSelectorButtonSelector); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index dc083605c2..4f0115bcc9 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -210,29 +210,35 @@ module.exports = () => { }); it('dynamically switches between visible pages in the page selector', async () => { - await goToPage(page, 'lhc-fill-overview'); - - // turn off Stable Beams Only filter - await pressElement(page, '.slider.round'); - + // The page selector of phones (screens narrower than 768px) only displays the three page buttons nearest to the current one + const viewport = page.viewport(); + await page.setViewport({ ...viewport, width: 800 }); + try { + await goToPage(page, 'lhc-fill-overview'); - // Override the amount of lhc fills visible per page manually - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.lhcFills.overviewModel.pagination.itemsPerPage = 1; - }); - await waitForTableLength(page, 1); + // turn off Stable Beams Only filter + await pressElement(page, '.slider.round'); - // Expect the page five button to now be visible, but no more than that - const pageFiveButton = await page.$('#page5'); - expect(pageFiveButton).to.be.not.null; - const pageSixButton = await page.$('#page6'); - expect(pageSixButton).to.be.null; - // Expect the page one button to have fallen away when clicking on page five button - await pressElement(page, '#page5'); - await page.waitForSelector('#page1', { hidden: true }); - }); + // Override the amount of lhc fills visible per page manually + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.lhcFills.overviewModel.pagination.itemsPerPage = 1; + }); + await waitForTableLength(page, 1); + + // Expect the page five button to now be visible, but no more than that + const pageFiveButton = await page.$('#page5'); + expect(pageFiveButton).to.be.not.null; + const pageSixButton = await page.$('#page6'); + expect(pageSixButton).to.be.null; + + // Expect the page one button to have fallen away when clicking on page five button + await pressElement(page, '#page5'); + await page.waitForSelector('#page1', { hidden: true }); + } finally { + await page.setViewport(viewport); + } }); it('should successfully navigate to the LHC fill details page', async () => { await goToPage(page, 'lhc-fill-overview'); diff --git a/test/public/lhcPeriods/overview.test.js b/test/public/lhcPeriods/overview.test.js index b203a1677a..440050ad82 100644 --- a/test/public/lhcPeriods/overview.test.js +++ b/test/public/lhcPeriods/overview.test.js @@ -86,7 +86,7 @@ module.exports = () => { await goToPage(page, 'lhc-period-overview'); // Expect the amount selector to currently be set to 11 (because of the defined page height) - await expectInnerText(page, '.dropup button', 'Rows per page: 11 '); + await expectInnerText(page, '.dropup button', 'Rows: 11 '); // Expect the dropdown options to be visible when it is selected await pressElement(page, '.dropup button'); @@ -96,7 +96,7 @@ module.exports = () => { // Expect the amount selector to currently be set to 5 when the first option (5) is selected await pressElement(page, '.dropup .menu-item'); - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + await expectInnerText(page, '.dropup button', 'Rows: 5 '); await waitForTableLength(page, 3); // Expect the custom per page input to have red border and text color if wrong value typed diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index bc61769581..ee173b4259 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -246,6 +246,43 @@ module.exports = () => { } }); + it('should adapt the pagination to small screens', async () => { + const getPaginationLayout = () => page.$eval('.pagination-bar', (bar) => { + const isDisplayed = (selector) => Boolean(bar.querySelector(selector).offsetParent); + const tops = ['.pagination-amount', '#pageSelector', '.pagination-count'].map((selector) => + Math.round(bar.querySelector(selector).getBoundingClientRect().top)); + return { + overflows: bar.scrollWidth > bar.clientWidth, + onOneLine: new Set(tops).size === 1, + firstAndLastButtonsDisplayed: isDisplayed('#pageMoveFirst') && isDisplayed('#pageMoveLast'), + displayedPageButtonsCount: [...bar.querySelectorAll('#pageSelector .btn-tab')].filter(({ offsetParent }) => offsetParent).length, + rowsCount: bar.querySelector('.pagination-count').innerText.replace(/\s+/g, ''), + }; + }); + + try { + await page.setViewport({ width: 375, height: 812, deviceScaleFactor: 1 }); + await goToPage(page, 'log-overview'); + await page.waitForSelector('.pagination-bar #pageSelector'); + const phoneLayout = await getPaginationLayout(); + expect(phoneLayout).to.include({ + overflows: false, + onOneLine: true, + firstAndLastButtonsDisplayed: false, + displayedPageButtonsCount: 3, + }); + expect(phoneLayout.rowsCount).to.match(/^\d+-\d+\/\d+$/); + + await page.setViewport({ width: 800, height: 1024, deviceScaleFactor: 1 }); + const tabletLayout = await getPaginationLayout(); + expect(tabletLayout).to.include({ overflows: false, onOneLine: true, firstAndLastButtonsDisplayed: true }); + expect(tabletLayout.displayedPageButtonsCount).to.be.greaterThan(3); + expect(tabletLayout.rowsCount).to.match(/^Rows\d+-\d+of\d+$/); + } finally { + await page.setViewport({ width: 1400, height: 940, deviceScaleFactor: 1 }); + } + }); + it('can sort by columns in ascending and descending manners', async () => { // Close the filter panel await waitForFirstRowToHaveId(page, 'row119'); diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 362faf7273..2a439b9d26 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -202,7 +202,7 @@ module.exports = () => { await pressElement(page, amountItems5); // Expect the amount selector to currently be set to 5 when the first option (5) is selected - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + await expectInnerText(page, '.dropup button', 'Rows: 5 '); await waitForTableLength(page, 5); // Expect the custom per page input to have red border and text color if wrong value typed diff --git a/test/public/runsPerPeriodOrPass/runsPerDataPass.overview.test.js b/test/public/runsPerPeriodOrPass/runsPerDataPass.overview.test.js index 148f23fd3a..ce1ac61d25 100644 --- a/test/public/runsPerPeriodOrPass/runsPerDataPass.overview.test.js +++ b/test/public/runsPerPeriodOrPass/runsPerDataPass.overview.test.js @@ -318,7 +318,7 @@ module.exports = () => { // Expect the amount of visible runs to reduce when the first option (5) is selected await waitForTableLength(page, 4); await page.waitForSelector('.dropup button'); - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + await expectInnerText(page, '.dropup button', 'Rows: 5 '); // Expect the custom per page input to have red border and text color if wrong value typed const customPerPageInput = await page.waitForSelector(`${amountSelectorId} input[type=number]`); diff --git a/test/public/runsPerPeriodOrPass/runsPerLhcPeriod.overview.test.js b/test/public/runsPerPeriodOrPass/runsPerLhcPeriod.overview.test.js index ddadf1c2ce..41b727d938 100644 --- a/test/public/runsPerPeriodOrPass/runsPerLhcPeriod.overview.test.js +++ b/test/public/runsPerPeriodOrPass/runsPerLhcPeriod.overview.test.js @@ -163,7 +163,7 @@ module.exports = () => { await fillInput(page, `${amountSelectorId} input[type=number]`, '3', ['input', 'change']); await waitForTableLength(page, 3); - await expectInnerText(page, '.dropup button', 'Rows per page: 3 '); + await expectInnerText(page, '.dropup button', 'Rows: 3 '); await pressElement(page, amountSelectorButtonSelector); await page.waitForSelector(`${amountSelectorId} .dropup-menu`); @@ -172,7 +172,7 @@ module.exports = () => { await pressElement(page, amountItems5, true); // only 4 runs in LHC Period 1 await waitForTableLength(page, 4); - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + await expectInnerText(page, '.dropup button', 'Rows: 5 '); // Expect the custom per page input to have red border and text color if wrong value typed await fillInput(page, `${amountSelectorId} input[type=number]`, '1111'); diff --git a/test/public/simulationPasses/overviewPerLhcPeriod.test.js b/test/public/simulationPasses/overviewPerLhcPeriod.test.js index 71e8371c9e..8649a29cfd 100644 --- a/test/public/simulationPasses/overviewPerLhcPeriod.test.js +++ b/test/public/simulationPasses/overviewPerLhcPeriod.test.js @@ -118,7 +118,7 @@ module.exports = () => { await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); // Expect the amount selector to currently be set to 10 (because of the defined page height) - await expectInnerText(page, '.dropup button', 'Rows per page: 9 '); + await expectInnerText(page, '.dropup button', 'Rows: 9 '); // Expect the dropdown options to be visible when it is selected await pressElement(page, '.dropup button');