This is an automated email from the ASF dual-hosted git repository. kinow pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit 0f1ee0aae572e59239e676c3e31f55af17c1256c Author: Bruno P. Kinoshita <[email protected]> AuthorDate: Sat Feb 17 22:44:03 2024 +0100 GH-2267: Display all the pagination options --- .../src/components/dataset/Pagination.vue | 251 ++++++++++++--------- .../src/components/dataset/TableListing.vue | 4 +- .../jena-fuseki-ui/src/views/dataset/Info.vue | 7 +- .../unit/components/dataset/pagination.vue.spec.js | 125 ++++++++++ 4 files changed, 278 insertions(+), 109 deletions(-) diff --git a/jena-fuseki2/jena-fuseki-ui/src/components/dataset/Pagination.vue b/jena-fuseki2/jena-fuseki-ui/src/components/dataset/Pagination.vue index 2c460a3a48..0375b7883a 100644 --- a/jena-fuseki2/jena-fuseki-ui/src/components/dataset/Pagination.vue +++ b/jena-fuseki2/jena-fuseki-ui/src/components/dataset/Pagination.vue @@ -16,110 +16,124 @@ --> <template> - <div class="row g-0"> - <div class="col-12"> - <ul - role="menubar" - aria-disabled="false" - aria-label="Pagination" - class="pagination mb-2 mx-0 justify-content-center" + <ul + role="menubar" + aria-disabled="false" + aria-label="Pagination" + class="pagination flex-wrap mb-2 mx-0 p-0 justify-content-center" + > + <!-- pages backward controls --> + <li + :aria-hidden="isBackLinkAriaDisabled" + :class="getBackLinkClass('page-item')" + role="presentation" + > + <button + :aria-disabled="isBackLinkAriaDisabled" + :class="getBackLinkClass('page-link')" + @click="goToPage(1)" + type="button" + role="menuitem" + tabindex="-1" + aria-label="Go to first page" > - <!-- pages backward controls --> - <li - :aria-hidden="getBackLinkAriaDisabled()" - :class="getBackLinkClass('page-item')" - role="presentation" - > - <button - :aria-disabled="getBackLinkAriaDisabled()" - :class="getBackLinkClass('page-link')" - @click="goToPage(1)" - type="button" - role="menuitem" - tabindex="-1" - aria-label="Go to first page" - > - « - </button> - </li> - <li - :aria-hidden="getBackLinkAriaDisabled()" - :class="getBackLinkClass('page-item')" - role="presentation" - > - <button - :aria-disabled="getBackLinkAriaDisabled()" - :class="getBackLinkClass('page-link')" - @click="goToPage(currentPage - 1)" - type="button" - aria-label="Go to previous page" - role="menuitem" - > - ‹ - </button> - </li> - <!-- pages --> - <li - v-for="page in numberOfPages" - :key="page" - role="presentation" - class="page-item" - > - <span - :aria-label="`Go to page ${ page }`" - :class="getPageLinkClass(page)" - :aria-checked="page === currentPage" - @click="goToPage(page)" - role="menuitemradio" - type="button" - aria-posinset="1" - aria-setsize="2" - tabindex="0" - >{{ page }}</span> - </li> - <!-- pages forward controls --> - <li - :aria-hidden="getNextLinkAriaDisabled()" - :class="getNextLinkClass('page-item')" - role="presentation" - > - <button - :aria-disabled="getNextLinkAriaDisabled()" - :class="getNextLinkClass('page-link')" - @click="goToPage(currentPage + 1)" - type="button" - role="menuitem" - aria-label="Go to last page" - > - › - </button> - </li> - <li - :aria-hidden="getNextLinkAriaDisabled()" - :class="getNextLinkClass('page-item')" - role="presentation" - > - <button - :aria-disabled="getNextLinkAriaDisabled()" - :class="getNextLinkClass('page-link')" - @click="goToPage(numberOfPages)" - type="button" - role="menuitem" - aria-label="Go to next page" - > - » - </button> - </li> - </ul> - </div> - </div> + « + </button> + </li> + <li + :aria-hidden="isBackLinkAriaDisabled" + :class="getBackLinkClass('page-item')" + role="presentation" + > + <button + :aria-disabled="isBackLinkAriaDisabled" + :class="getBackLinkClass('page-link')" + @click="goToPage(currentPage - 1)" + type="button" + aria-label="Go to previous page" + role="menuitem" + > + ‹ + </button> + </li> + <li + v-if="isBackSummaryEnabled" + role="presentation" + class="page-link page-summary-item" + > + <span>…</span> + </li> + <!-- pages --> + <li + v-for="page in pages" + :key="page" + role="presentation" + class="page-item" + > + <span + :aria-label="`Go to page ${ page }`" + :class="getPageLinkClass(page)" + :aria-checked="page === currentPage" + @click="goToPage(page)" + role="menuitemradio" + type="button" + aria-posinset="1" + aria-setsize="2" + tabindex="0" + >{{ page }}</span> + </li> + <!-- pages forward controls --> + <li + v-if="isAfterSummaryEnabled" + role="presentation" + class="page-link page-summary-item" + > + <span>…</span> + </li> + <li + :aria-hidden="isNextLinkAriaDisabled" + :class="getNextLinkClass('page-item')" + role="presentation" + > + <button + :aria-disabled="isNextLinkAriaDisabled" + :class="getNextLinkClass('page-link')" + @click="goToPage(currentPage + 1)" + type="button" + role="menuitem" + aria-label="Go to last page" + > + › + </button> + </li> + <li + :aria-hidden="isNextLinkAriaDisabled" + :class="getNextLinkClass('page-item')" + role="presentation" + > + <button + :aria-disabled="isNextLinkAriaDisabled" + :class="getNextLinkClass('page-link')" + @click="goToPage(numberOfPages)" + type="button" + role="menuitem" + aria-label="Go to next page" + > + » + </button> + </li> + </ul> </template> <script> +function range (size, startAt) { + return [...Array(size).keys()].map(i => i + startAt) +} + export default { name: 'Pagination', - prop: { + props: { totalRows: { type: Number, required: true @@ -132,15 +146,48 @@ export default { perPage: { type: Number, default: 5 + }, + /* Maximum pages displayed. */ + maxDisplayed: { + type: Number, + default: 5 } }, computed: { currentPage () { - return this.$attrs.value + return this.value }, numberOfPages () { - return Math.ceil(this.$attrs['total-rows'] / this.$attrs['per-page']) || 0 + return Math.ceil(this.totalRows / this.perPage) || 0 + }, + isSummaryEnabled () { + return this.numberOfPages > this.maxDisplayed + }, + pages () { + if (this.totalRows === 0) { + return [] + } + const toBeDisplayed = Math.min(this.maxDisplayed, this.numberOfPages) + if (!this.isBackSummaryEnabled) { + return range(toBeDisplayed, 1) + } + if (!this.isAfterSummaryEnabled) { + return range(toBeDisplayed, this.numberOfPages - (this.maxDisplayed - 1)) + } + return range(toBeDisplayed, this.currentPage - Math.floor(this.maxDisplayed / 2)) + }, + isBackLinkAriaDisabled () { + return this.currentPage === 1 || this.numberOfPages === 0 + }, + isNextLinkAriaDisabled () { + return this.currentPage === this.numberOfPages || this.numberOfPages === 0 + }, + isBackSummaryEnabled () { + return this.isSummaryEnabled && this.currentPage - Math.ceil(this.maxDisplayed / 2) > 0 + }, + isAfterSummaryEnabled () { + return this.isSummaryEnabled && this.currentPage + Math.floor(this.maxDisplayed / 2) < this.numberOfPages } }, @@ -165,17 +212,11 @@ export default { disabled: this.currentPage === 1 || this.numberOfPages === 0 } }, - getBackLinkAriaDisabled () { - return this.currentPage === 1 || this.numberOfPages === 0 - }, getNextLinkClass (mainClass) { return { [mainClass]: true, disabled: this.currentPage === this.numberOfPages || this.numberOfPages === 0 } - }, - getNextLinkAriaDisabled () { - return this.currentPage === this.numberOfPages || this.numberOfPages === 0 } } } diff --git a/jena-fuseki2/jena-fuseki-ui/src/components/dataset/TableListing.vue b/jena-fuseki2/jena-fuseki-ui/src/components/dataset/TableListing.vue index cb566ff341..a0f188559b 100644 --- a/jena-fuseki2/jena-fuseki-ui/src/components/dataset/TableListing.vue +++ b/jena-fuseki2/jena-fuseki-ui/src/components/dataset/TableListing.vue @@ -70,8 +70,8 @@ </jena-table> </div> </div> - <div class="row g-0"> - <div class="col-12"> + <div class="row"> + <div class="col-12 p-0"> <pagination :value="currentPage" :per-page="perPage" diff --git a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Info.vue b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Info.vue index 286edc0b81..4e43adba91 100644 --- a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Info.vue +++ b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Info.vue @@ -97,10 +97,11 @@ count triples in all graphs </button> </div> - <jena-table + <table-listing :fields="countGraphFields" :items="countGraphItems" :busy="isDatasetSizeLoading" + :filterable="false" id="dataset-size-table" class="mt-3" bordered @@ -118,7 +119,7 @@ <template #empty> <span>No data</span> </template> - </jena-table> + </table-listing> </div> </div> </div> @@ -168,11 +169,13 @@ import currentDatasetMixin from '@/mixins/current-dataset' import currentDatasetMixinNavigationGuards from '@/mixins/current-dataset-navigation-guards' import { Popover } from 'bootstrap' import JenaTable from '@/components/dataset/JenaTable.vue' +import TableListing from "@/components/dataset/TableListing.vue"; export default { name: 'DatasetInfo', components: { + TableListing, JenaTable, Menu }, diff --git a/jena-fuseki2/jena-fuseki-ui/tests/unit/components/dataset/pagination.vue.spec.js b/jena-fuseki2/jena-fuseki-ui/tests/unit/components/dataset/pagination.vue.spec.js new file mode 100644 index 0000000000..f71fd6bc40 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-ui/tests/unit/components/dataset/pagination.vue.spec.js @@ -0,0 +1,125 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {describe, expect, it} from 'vitest' +import {mount} from '@vue/test-utils' +import Pagination from '@/components/dataset/Pagination.vue' + + +describe('Pagination', () => { + // Disabled first, disabled back, disabled next, disabled last. + const MINIMUM_PAGE_ITEMS = 4 + /** + * @param options + * @returns {VueWrapper<Pagination>} + */ + const mountFunction = options => { + return mount(Pagination, { + ...options, + global: { + mocks: { + $fusekiService: {} + } + } + }) + } + it('works when there are no rows, no pages', () => { + const component = mountFunction({ + propsData: { + totalRows: 0, + value: 0 + } + }) + expect(component.find('.pagination')).to.exist + expect(component.findAll('.page-item')).toHaveLength(MINIMUM_PAGE_ITEMS) + expect(component.findAll('.page-summary-item')).toHaveLength(0) + }) + it('works when there is a single page', () => { + // Given we have a maximum of seven items, we test that it works + // as expected for 1 row, 2 rows, 3 rows, ..., 7 rows. For all + // these cases we must have the same number of pages in this + // Pagination component: just one. + const maxDisplayed = 7 + for (let totalRows = 1; totalRows <= maxDisplayed; ++totalRows) { + const component = mountFunction({ + propsData: { + totalRows: totalRows, + perPage: totalRows, + maxDisplayed: maxDisplayed, + value: 1 + } + }) + expect(component.find('.pagination')).to.exist + expect(component.findAll('.page-item')).toHaveLength(1 + MINIMUM_PAGE_ITEMS) + expect(component.findAll('.page-summary-item')).toHaveLength(0) + } + }) + it('works when there is more than 1 page', () => { + // We display one item per page. + const perPage = 1 + // We display a maximum of 1 page. + const maxDisplayed = 1 + // We have 3 items (rows). + const totalRows = 3 + + // This covers all cases, with no back summary (...), with back and next summary, and with the next summary only. + for (let currentPage = 1; currentPage <= totalRows; ++currentPage) { + const component = mountFunction({ + propsData: { + totalRows: totalRows, + perPage: perPage, + value: currentPage, + maxDisplayed: maxDisplayed + } + }) + expect(component.find('.pagination')).to.exist + // We will display the maxDisplayed (2) plus the minimum page items. + expect(component.findAll('.page-item')).toHaveLength(maxDisplayed + MINIMUM_PAGE_ITEMS) + // For the first page we will display the back summary. + if (currentPage === 1) { + expect(component.findAll('.page-summary-item')).toHaveLength(1) + } + // The second page will show the summary for back and next. + if (currentPage === 2) { + expect(component.findAll('.page-summary-item')).toHaveLength(2) + } + // Lastly, the third page shows only the back summary. + if (currentPage === 3) { + expect(component.findAll('.page-summary-item')).toHaveLength(1) + } + } + }) + it('fires an event when you go to another page', async () => { + const component = mountFunction({ + propsData: { + totalRows: 1 + } + }) + + const theAnswer = 42 + + component.vm.goToPage(theAnswer) + + // Wait until $emits have been handled + await component.vm.$nextTick() + + const inputEventsEmitted = component.emitted().input[0] + const singleEventEmitted = inputEventsEmitted[0] + + expect(singleEventEmitted).toBe(theAnswer) + }) +})
