feat: improved page navigation for users list (#3741)

* feat: add first and last page buttons

* feat: add textbox-based page navigation

* feat: add query parameter page navigation
This commit is contained in:
David Wheatley 2023-02-21 13:04:30 +00:00 committed by GitHub
parent 0da069ba9f
commit 408a92b4ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 117 additions and 8 deletions

View File

@ -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<IPageAttrs, this>) {
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 [
<section class="UserListPage-grid UserListPage-grid--loading">
@ -149,6 +171,13 @@ export default class UserListPage extends AdminPage {
{this.isLoadingPage && <LoadingIndicator size="large" />}
</section>,
<nav class="UserListPage-gridPagination">
<Button
disabled={this.pageNumber === 0}
title={app.translator.trans('core.admin.users.pagination.first_page_button')}
onclick={this.goToPage.bind(this, 1)}
icon="fas fa-step-backward"
className="Button Button--icon UserListPage-firstPageBtn"
/>
<Button
disabled={this.pageNumber === 0}
title={app.translator.trans('core.admin.users.pagination.back_button')}
@ -158,7 +187,38 @@ export default class UserListPage extends AdminPage {
/>
<span class="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.pageNumber + 1,
current: (
<input
type="text"
value={this.loadingPageNumber + 1}
aria-label={extractText(app.translator.trans('core.admin.users.pagination.go_to_page_textbox_a11y_label'))}
autocomplete="off"
className="FormControl UserListPage-pageNumberInput"
onchange={(e: InputEvent) => {
const target = e.target as HTMLInputElement;
let pageNumber = parseInt(target.value);
if (isNaN(pageNumber)) {
// Invalid value, reset to current page
target.value = (this.pageNumber + 1).toString();
return;
}
if (pageNumber < 1) {
// Lower constraint
pageNumber = 1;
} else if (pageNumber > this.getTotalPageCount()) {
// Upper constraint
pageNumber = this.getTotalPageCount();
}
target.value = pageNumber.toString();
this.goToPage(pageNumber);
}}
/>
),
currentNum: this.pageNumber + 1,
total: this.getTotalPageCount(),
})}
</span>
@ -169,6 +229,13 @@ export default class UserListPage extends AdminPage {
icon="fas fa-chevron-right"
className="Button Button--icon UserListPage-nextBtn"
/>
<Button
disabled={!this.moreData}
title={app.translator.trans('core.admin.users.pagination.last_page_button')}
onclick={this.goToPage.bind(this, this.getTotalPageCount())}
icon="fas fa-step-forward"
className="Button Button--icon UserListPage-lastPageBtn"
/>
</nav>,
];
}
@ -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<User[]>('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();
}
}

View File

@ -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

View File

@ -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}