Skip to content

Commit

Permalink
feat: add page size support for pagination (#336)
Browse files Browse the repository at this point in the history
# What ❔

Add page size dropdown to show `10`, `20`, `50`, or `100` records in
paginated tables

## Why ❔

Allows users to see more data on a single page.

Closes #215 

## Checklist

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [x] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [x] Tests for the changes have been added / updated.
- [x] Documentation comments have been added / updated.

## Evidence


https://github.com/user-attachments/assets/77d0aefe-f4ba-4f22-b765-5b3e8872e1a7
  • Loading branch information
MexicanAce authored Nov 28, 2024
1 parent 5fc513a commit 0d144bc
Show file tree
Hide file tree
Showing 18 changed files with 229 additions and 103 deletions.
8 changes: 6 additions & 2 deletions packages/app/src/components/common/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<ChevronDownIcon class="toggle-button-icon" :class="{ 'toggle-button-opened': open }" />
</span>
</ListboxButton>
<ListboxOptions class="options-list-container">
<ListboxOptions class="options-list-container" :class="showAbove ? 'mb-1 bottom-full' : 'top-full'">
<ListboxOption
as="template"
v-for="option in options"
Expand Down Expand Up @@ -75,6 +75,10 @@ const props = defineProps({
type: String,
default: "",
},
showAbove: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
Expand Down Expand Up @@ -124,7 +128,7 @@ const selected = computed({
@apply mt-0.5 text-sm text-error-500;
}
.options-list-container {
@apply absolute z-10 mt-1 max-h-[180px] w-full cursor-pointer overflow-hidden overflow-y-auto rounded-md border-neutral-300 bg-white text-sm shadow-md focus:outline-none;
@apply absolute z-20 mt-1 max-h-[180px] w-full cursor-pointer overflow-hidden overflow-y-auto rounded-md border-neutral-300 bg-white text-sm shadow-md focus:outline-none;
.options-list-item {
@apply px-3 py-3 hover:bg-neutral-100;
Expand Down
186 changes: 126 additions & 60 deletions packages/app/src/components/common/Pagination.vue
Original file line number Diff line number Diff line change
@@ -1,58 +1,99 @@
<template>
<nav class="pagination-container" :class="{ disabled }" aria-label="Pagination">
<PaginationButton
:use-route="useQuery"
:to="{ query: backButtonQuery, hash: currentHash }"
class="pagination-page-button arrow left"
:aria-disabled="isFirstPage"
:class="{ disabled: isFirstPage }"
@click="currentPage = Math.max(currentPage - 1, 1)"
>
<span class="sr-only">Previous</span>
<ChevronLeftIcon class="chevron-icon" aria-hidden="true" />
</PaginationButton>
<template v-for="(item, index) in pagesData" :key="index">
<div class="pagination-container">
<div class="page-size-container">
<Dropdown
class="page-size-dropdown"
v-model="computedPageSize"
:options="pageSizeOptions"
:show-above="true"
:pending="disabled"
/>
<span class="page-size-text">{{ t("pagination.records") }}</span>
</div>

<nav class="page-numbers-container" :class="{ disabled }" aria-label="Pagination">
<PaginationButton
:use-route="useQuery"
:to="{ query: backButtonQuery, hash: currentHash }"
class="pagination-page-button arrow left"
:aria-disabled="isFirstPage"
:class="{ disabled: isFirstPage }"
@click="currentPage = Math.max(currentPage - 1, 1)"
>
<span class="sr-only">Previous</span>
<ChevronLeftIcon class="chevron-icon" aria-hidden="true" />
</PaginationButton>
<template v-for="(item, index) in pagesData" :key="index">
<PaginationButton
v-if="item.type === 'page'"
:use-route="useQuery"
:to="{
query: item.number > 1 ? { page: item.number, pageSize: computedPageSize } : { pageSize: computedPageSize },
hash: currentHash,
}"
:aria-current="activePage === item.number ? 'page' : 'false'"
:class="[{ active: activePage === item.number }, item.hiddenOnMobile ? 'hidden sm:flex' : 'flex']"
class="pagination-page-button page"
@click="currentPage = item.number"
>
{{ item.number }}
</PaginationButton>
<span v-else class="pagination-page-button dots">...</span>
</template>

<PaginationButton
v-if="item.type === 'page'"
:use-route="useQuery"
:to="{ query: item.number > 1 ? { page: item.number } : {}, hash: currentHash }"
:aria-current="activePage === item.number ? 'page' : 'false'"
:class="[{ active: activePage === item.number }, item.hiddenOnMobile ? 'hidden sm:flex' : 'flex']"
class="pagination-page-button page"
@click="currentPage = item.number"
:to="{ query: nextButtonQuery, hash: currentHash }"
class="pagination-page-button arrow right"
:aria-disabled="isLastPage"
:class="{ disabled: isLastPage }"
@click="currentPage = Math.min(currentPage + 1, pageCount)"
>
{{ item.number }}
<span class="sr-only">Next</span>
<ChevronRightIcon class="chevron-icon" aria-hidden="true" />
</PaginationButton>
<span v-else class="pagination-page-button dots">...</span>
</template>

<PaginationButton
:use-route="useQuery"
:to="{ query: nextButtonQuery, hash: currentHash }"
class="pagination-page-button arrow right"
:aria-disabled="isLastPage"
:class="{ disabled: isLastPage }"
@click="currentPage = Math.min(currentPage + 1, pageCount)"
>
<span class="sr-only">Next</span>
<ChevronRightIcon class="chevron-icon" aria-hidden="true" />
</PaginationButton>
</nav>
</nav>
</div>
</template>

<script setup lang="ts">
import { computed, type UnwrapNestedRefs } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/outline";
import { useOffsetPagination, type UseOffsetPaginationReturn } from "@vueuse/core";
import Dropdown from "@/components/common/Dropdown.vue";
import PaginationButton from "@/components/common/PaginationButton.vue";
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const currentHash = computed(() => route?.hash);
const pageSizeOptions = ["10", "20", "50", "100"];
const computedPageSize = computed({
get() {
return currentPageSize.value.toString();
},
set(newValue) {
const parsedValue = parseInt(newValue, 10);
if (parsedValue !== currentPageSize.value) {
currentPageSize.value = parsedValue;
router.push({
query: {
page: currentPage.value,
pageSize: parsedValue,
},
hash: currentHash.value,
});
}
},
});
const props = defineProps({
activePage: {
type: Number,
Expand All @@ -78,20 +119,23 @@ const props = defineProps({
const emit = defineEmits<{
(eventName: "update:activePage", value: number): void;
(eventName: "update:pageSize", value: number): void;
(eventName: "onPageChange", value: UnwrapNestedRefs<UseOffsetPaginationReturn>): void;
(eventName: "onPageSizeChange", value: UnwrapNestedRefs<UseOffsetPaginationReturn>): void;
}>();
const { currentPage, pageCount, isFirstPage, isLastPage } = useOffsetPagination({
const { currentPage, currentPageSize, pageCount, isFirstPage, isLastPage } = useOffsetPagination({
total: computed(() => props.totalItems),
page: computed({
get: () => props.activePage,
set: (val) => emit("update:activePage", val),
}),
pageSize: computed({
get: () => props.pageSize,
set: () => undefined,
set: (val) => emit("update:pageSize", val),
}),
onPageChange: (data) => emit("onPageChange", data),
onPageSizeChange: (data) => emit("onPageSizeChange", data),
});
const pagesData = computed(() => {
Expand Down Expand Up @@ -123,36 +167,58 @@ const pagesData = computed(() => {
return [...first, ...middle, ...last];
});
const backButtonQuery = computed(() => (currentPage.value > 2 ? { page: currentPage.value - 1 } : {}));
const nextButtonQuery = computed(() => ({ page: Math.min(currentPage.value + 1, pageCount.value) }));
const backButtonQuery = computed(() =>
currentPage.value > 2
? { page: currentPage.value - 1, pageSize: computedPageSize.value }
: { pageSize: computedPageSize.value }
);
const nextButtonQuery = computed(() => ({
page: Math.min(currentPage.value + 1, pageCount.value),
pageSize: computedPageSize.value,
}));
</script>

<style lang="scss">
.pagination-container {
@apply flex space-x-1 transition-opacity;
&.disabled {
@apply pointer-events-none opacity-50;
}
.pagination-page-button {
@apply rounded-md bg-white px-1.5 py-1 font-mono text-sm font-medium text-neutral-700 no-underline sm:px-2;
&:not(.disabled):not(.active):not(.dots) {
@apply hover:bg-neutral-50;
}
@apply flex flex-col-reverse items-center justify-center relative sm:flex-row;
.page-numbers-container {
@apply flex space-x-1 transition-opacity justify-center p-3;
&.disabled {
@apply cursor-not-allowed text-neutral-400;
@apply pointer-events-none opacity-50;
}
&.active {
@apply z-10 bg-neutral-100;
.pagination-page-button {
@apply rounded-md bg-white px-1.5 py-1 font-mono text-sm font-medium text-neutral-700 no-underline sm:px-2;
&:not(.disabled):not(.active):not(.dots) {
@apply hover:bg-neutral-50;
}
&.disabled {
@apply cursor-not-allowed text-neutral-400;
}
&.active {
@apply z-10 bg-neutral-100;
}
&.dots {
@apply font-sans text-neutral-400 hover:bg-white;
}
&.arrow {
@apply flex items-center;
.chevron-icon {
@apply h-4 w-4;
}
}
}
&.dots {
@apply font-sans text-neutral-400 hover:bg-white;
}
.page-size-container {
@apply relative left-0 flex items-center justify-center pb-2 sm:absolute sm:pb-0 sm:left-6;
.page-size-text {
@apply text-gray-500 pl-2;
}
&.arrow {
@apply flex items-center;
.chevron-icon {
@apply h-4 w-4;
.page-size-dropdown {
@apply w-16;
.toggle-button {
@apply py-0 px-2 leading-8 h-8;
}
}
}
Expand Down
27 changes: 11 additions & 16 deletions packages/app/src/components/transactions/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,13 @@
</TableBodyColumn>
</template>
<template v-if="pagination && total && total > pageSize && transactions?.length" #footer>
<div class="pagination">
<Pagination
v-model:active-page="activePage"
:use-query="useQueryPagination"
:total-items="total!"
:page-size="pageSize"
:disabled="isLoading"
/>
</div>
<Pagination
v-model:active-page="activePage"
:use-query="useQueryPagination"
:total-items="total!"
:page-size="pageSize"
:disabled="isLoading"
/>
</template>
<template #loading>
<tr class="loader-row" v-for="row in pageSize" :key="row">
Expand Down Expand Up @@ -265,9 +263,10 @@ const activePage = ref(props.useQueryPagination ? parseInt(route.query.page as s
const toDate = new Date();
watch(
[activePage, searchParams],
([page]) => {
load(page, toDate);
[activePage, () => route.query.pageSize, searchParams],
([page, pageSize]) => {
const currentPageSize = pageSize ? parseInt(pageSize as string) : 10;
load(page, toDate, currentPageSize);
},
{ immediate: true }
);
Expand Down Expand Up @@ -462,10 +461,6 @@ function getDirection(item: TransactionListItem): Direction {
@apply pr-2 normal-case normal-case;
}
.pagination {
@apply flex justify-center p-3;
}
.table-body {
@apply rounded-t-lg;
th.table-head-col {
Expand Down
9 changes: 6 additions & 3 deletions packages/app/src/composables/common/useFetchCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type UseFetchCollection<T> = {
page: ComputedRef<null | number>;
pageSize: ComputedRef<number>;

load: (nextPage: number, toDate?: Date) => Promise<void>;
load: (nextPage: number, toDate?: Date, pageSize?: number) => Promise<void>;
};

export function useFetchCollection<T, TApiResponse = T>(
Expand All @@ -29,15 +29,18 @@ export function useFetchCollection<T, TApiResponse = T>(
const page = ref<null | number>(null);
const total = ref<null | number>(null);

async function load(nextPage: number, toDate?: Date) {
async function load(nextPage: number, toDate?: Date, updatedPageSize?: number) {
page.value = nextPage;
if (updatedPageSize) {
pageSize.value = updatedPageSize;
}

pending.value = true;
failed.value = false;

try {
const url = typeof resource === "function" ? resource() : resource;
url.searchParams.set("pageSize", pageSize.value.toString());
url.searchParams.set("limit", pageSize.value.toString());
url.searchParams.set("page", nextPage.toString());

if (toDate && +new Date(toDate) > 0) {
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,5 +730,8 @@
"systemAlert": {
"indexerDelayed": "Transaction indexing is {indexerDelayInHours} hours behind. Transactions are being processed normally and will gradually show up. You can also use other <a href=\"https://zksync.io/explore/\" style=\"color: inherit\">explorers</a> meanwhile.",
"indexerDelayedDueToHeavyLoad": "The network is under a heavy load at the moment and transaction indexing on the explorer is {indexerDelayInHours} hours behind. Transactions are being processed normally and will gradually show up. You can also use other <a href=\"https://zksync.io/explore/\" style=\"color: inherit\">explorers</a> meanwhile."
},
"pagination": {
"records": "Records"
}
}
3 changes: 3 additions & 0 deletions packages/app/src/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,8 @@
"systemAlert": {
"indexerDelayed": "Індексація транзакцій відстає на {indexerDelayInHours} годин. Транзакції будуть поступово оброблені та відображені. Ви також можете скористатися іншими <a href=\"https://zksync.io/explore/\" style=\"color: inherit\">блок експлорерами</a> наразі.",
"indexerDelayedDueToHeavyLoad": "Мережа наразі перебуває під великим навантаженням, індексація транзакцій відстає на {indexerDelayInHours} годин. Транзакції будуть поступово оброблені та відображені. Ви також можете скористатися іншими <a href=\"https://zksync.io/explore/\" style=\"color: inherit\">блок експлорерами</a> наразі."
},
"pagination" : {
"records": "записи"
}
}
Loading

0 comments on commit 0d144bc

Please sign in to comment.