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)
+  })
+})

Reply via email to