diff --git a/framework/core/js/src/admin/components/UserListPage.tsx b/framework/core/js/src/admin/components/UserListPage.tsx index 9d8bd11100..3eebe7f31a 100644 --- a/framework/core/js/src/admin/components/UserListPage.tsx +++ b/framework/core/js/src/admin/components/UserListPage.tsx @@ -10,6 +10,7 @@ import icon from '../../common/helpers/icon'; import listItems from '../../common/helpers/listItems'; import type User from '../../common/models/User'; +import type { IPageAttrs } from '../../common/components/Page'; import ItemList from '../../common/utils/ItemList'; import classList from '../../common/utils/classList'; @@ -45,6 +46,11 @@ export default class UserListPage extends AdminPage { */ private pageNumber: number = 0; + /** + * Page number being loaded. Zero-indexed. + */ + private loadingPageNumber: number = 0; + /** * Total number of forum users. * @@ -77,12 +83,28 @@ export default class UserListPage extends AdminPage { private isLoadingPage: boolean = false; + oninit(vnode: Mithril.Vnode) { + super.oninit(vnode); + + // Get page query value from URL + const page = parseInt(m.route.param('page')); + + if (isNaN(page) || page < 1) { + this.setPageNumberInUrl(1); + this.pageNumber = 0; + } else { + this.pageNumber = page - 1; + } + + this.loadingPageNumber = this.pageNumber; + } + /** * Component to render. */ content() { if (typeof this.pageData === 'undefined') { - this.loadPage(0); + this.loadPage(this.pageNumber); return [
@@ -149,6 +171,13 @@ export default class UserListPage extends AdminPage { {this.isLoadingPage && }
, , ]; } @@ -347,11 +414,14 @@ export default class UserListPage extends AdminPage { * * Uses the `this.numPerPage` as the response limit, and automatically calculates the offset required from `pageNumber`. * - * @param pageNumber The page number to load and display + * @param pageNumber The **zero-based** page number to load and display */ async loadPage(pageNumber: number) { if (pageNumber < 0) pageNumber = 0; + this.loadingPageNumber = pageNumber; + this.setPageNumberInUrl(pageNumber + 1); + app.store .find('users', { filter: { q: this.query }, @@ -369,9 +439,16 @@ export default class UserListPage extends AdminPage { // @ts-ignore delete data.payload; - this.pageData = data; - this.pageNumber = pageNumber; - this.isLoadingPage = false; + const lastPage = this.getTotalPageCount(); + + if (pageNumber > lastPage) { + this.loadPage(lastPage - 1); + } else { + this.pageData = data; + this.pageNumber = pageNumber; + this.loadingPageNumber = pageNumber; + this.isLoadingPage = false; + } m.redraw(); }) @@ -390,4 +467,20 @@ export default class UserListPage extends AdminPage { this.isLoadingPage = true; this.loadPage(this.pageNumber - 1); } + + /** + * @param page The **1-based** page number + */ + goToPage(page: number) { + this.isLoadingPage = true; + this.loadPage(page - 1); + } + + private setPageNumberInUrl(pageNumber: number) { + const search = window.location.hash.split('?', 2); + const params = new URLSearchParams(search?.[1] ?? ''); + + params.set('page', `${pageNumber}`); + window.location.hash = search?.[0] + '?' + params.toString(); + } } diff --git a/framework/core/less/admin/UsersListPage.less b/framework/core/less/admin/UsersListPage.less index b76b653f0d..907f2d8be0 100644 --- a/framework/core/less/admin/UsersListPage.less +++ b/framework/core/less/admin/UsersListPage.less @@ -69,11 +69,24 @@ } &-gridPagination { - display: flex; + display: grid; + grid-template-columns: auto auto 1fr auto auto; + gap: 8px; align-items: center; - justify-content: space-between; + justify-content: center; margin-top: 16px; } + + &-pageNumber { + text-align: center; + } + + &-pageNumberInput { + display: inline-block; + margin: 0 8px; + width: auto; + max-width: 80px; + } } // Handles styling of default UserList columns diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 997afb4dd3..2486ab74e4 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -273,6 +273,9 @@ core: pagination: back_button: Previous page + first_button: Go to first page + go_to_page_textbox_a11y_label: Go directly to page number + last_button: Go to last page next_button: Next page page_counter: Page {current} of {total}