1
0
mirror of https://github.com/flarum/core.git synced 2025-08-06 08:27:42 +02:00

feat: extension list UI (#4066)

This commit is contained in:
Sami Mazouz
2024-10-16 18:12:46 +01:00
committed by GitHub
parent b0e8f5ca36
commit 0107c96fb7
59 changed files with 1769 additions and 514 deletions

View File

@@ -16,13 +16,14 @@ import MailPage from './components/MailPage';
import AdvancedPage from './components/AdvancedPage';
import PermissionsPage from './components/PermissionsPage';
export type Extension = {
export interface Extension {
id: string;
name: string;
version: string;
description?: string;
icon?: {
name: string;
[key: string]: string;
};
links: {
authors?: {
@@ -44,7 +45,7 @@ export type Extension = {
};
};
require?: Record<string, string>;
};
}
export enum DatabaseDriver {
MySQL = 'MySQL',

View File

@@ -19,6 +19,7 @@ import CreateUserModal from './CreateUserModal';
import Icon from '../../common/components/Icon';
import Input from '../../common/components/Input';
import GambitsAutocompleteDropdown from '../../common/components/GambitsAutocompleteDropdown';
import Pagination from '../../common/components/Pagination';
type ColumnData = {
/**
@@ -78,11 +79,6 @@ export default class UserListPage extends AdminPage {
*/
private pageData: User[] | undefined = undefined;
/**
* Are there more users available?
*/
private moreData: boolean = false;
private isLoadingPage: boolean = false;
oninit(vnode: Mithril.Vnode<IPageAttrs, this>) {
@@ -160,76 +156,13 @@ export default class UserListPage extends AdminPage {
{/* Loading spinner that shows when a new page is being loaded */}
{this.isLoadingPage && <LoadingIndicator size="large" />}
</section>,
<nav className="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')}
onclick={this.previousPage.bind(this)}
icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn"
/>
<span className="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
current: (
<input
type="text"
inputmode="numeric"
pattern="[0-9]*"
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>
<Button
disabled={!this.moreData}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={this.nextPage.bind(this)}
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>,
<Pagination
currentPage={this.pageNumber + 1}
loadingPageNumber={this.loadingPageNumber + 1}
total={this.userCount}
perPage={this.numPerPage}
onChange={this.goToPage.bind(this)}
/>,
];
}
@@ -482,9 +415,6 @@ export default class UserListPage extends AdminPage {
},
})
.then((apiData) => {
// Next link won't be present if there's no more data
this.moreData = !!apiData.payload?.links?.next;
let data = apiData;
// @ts-ignore
@@ -509,16 +439,6 @@ export default class UserListPage extends AdminPage {
});
}
nextPage() {
this.isLoadingPage = true;
this.loadPage(this.pageNumber + 1);
}
previousPage() {
this.isLoadingPage = true;
this.loadPage(this.pageNumber - 1);
}
/**
* @param page The **1-based** page number
*/
@@ -532,6 +452,7 @@ export default class UserListPage extends AdminPage {
const params = new URLSearchParams(search?.[1] ?? '');
params.set('page', `${pageNumber}`);
window.location.hash = search?.[0] + '?' + params.toString();
// window.location.hash = search?.[0] + '?' + params.toString();
window.history.replaceState(null, '', search?.[0] + '?' + params.toString());
}
}

View File

@@ -4,6 +4,11 @@ import Model, { ModelData, SavedModelData } from './Model';
import GambitManager from './GambitManager';
export interface MetaInformation {
page?: {
limit?: number;
offset?: number;
total?: number;
};
[key: string]: any;
}

View File

@@ -31,6 +31,7 @@ import './utils/patchMithril';
import './utils/classList';
import './utils/extractText';
import './utils/formatNumber';
import './utils/formatAmount';
import './utils/mapRoutes';
import './utils/withAttr';
import './utils/focusTrap';

View File

@@ -14,7 +14,6 @@ export interface IInputAttrs extends ComponentAttrs {
clearable?: boolean;
clearLabel?: string;
loading?: boolean;
inputClassName?: string;
onchange?: (value: string) => void;
value?: string;
stream?: Stream<string>;

View File

@@ -0,0 +1,94 @@
import Component, { ComponentAttrs } from '../Component';
import Button from './Button';
import app from '../../admin/app';
import extractText from '../utils/extractText';
export interface IPaginationInterface extends ComponentAttrs {
total: number;
perPage: number;
currentPage: number;
loadingPageNumber?: number;
onChange: (page: number) => void;
}
export default class Pagination<CustomAttrs extends IPaginationInterface = IPaginationInterface> extends Component<CustomAttrs> {
view() {
const { total, perPage, currentPage, loadingPageNumber, onChange } = this.attrs;
const totalPageCount = Math.ceil(total / perPage);
const moreData = totalPageCount > currentPage;
return (
<nav className="Pagination">
<Button
disabled={currentPage === 1}
title={app.translator.trans('core.admin.users.pagination.first_page_button')}
onclick={() => onChange(1)}
icon="fas fa-step-backward"
className="Button Button--icon Pagination-first"
/>
<Button
disabled={currentPage === 1}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => onChange(currentPage - 1)}
icon="fas fa-chevron-left"
className="Button Button--icon Pagination-back"
/>
<span className="Pagination-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
current: (
<input
type="text"
inputmode="numeric"
pattern="[0-9]*"
value={loadingPageNumber ?? currentPage}
aria-label={extractText(app.translator.trans('core.admin.users.pagination.go_to_page_textbox_a11y_label'))}
autocomplete="off"
className="FormControl Pagination-input"
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 = (currentPage + 1).toString();
return;
}
if (pageNumber < 1) {
// Lower constraint
pageNumber = 1;
} else if (pageNumber > totalPageCount) {
// Upper constraint
pageNumber = totalPageCount;
}
target.value = pageNumber.toString();
onChange(pageNumber);
}}
/>
),
currentNum: currentPage,
total: totalPageCount,
})}
</span>
<Button
disabled={!moreData}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => onChange(currentPage + 1)}
icon="fas fa-chevron-right"
className="Button Button--icon Pagination-next"
/>
<Button
disabled={!moreData}
title={app.translator.trans('core.admin.users.pagination.last_page_button')}
onclick={() => onChange(totalPageCount)}
icon="fas fa-step-forward"
className="Button Button--icon Pagination-last"
/>
</nav>
);
}
}

View File

@@ -126,9 +126,9 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
tabs(): JSX.Element {
return (
<div className="SearchModal-tabs">
<div className="SearchModal-tabs-nav">{this.tabItems().toArray()}</div>
<div className="SearchModal-tabs-content">{this.activeTabItems().toArray()}</div>
<div className="Tabs">
<div className="Tabs-nav">{this.tabItems().toArray()}</div>
<div className="Tabs-content SearchModal-tabs-content">{this.activeTabItems().toArray()}</div>
</div>
);
}

View File

@@ -47,6 +47,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
protected location!: PaginationLocation;
public pageSize: number | null;
public totalItems: number | null = null;
protected pages: Page<T>[] = [];
protected params: P = {} as P;
@@ -54,6 +55,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
protected initialLoading: boolean = false;
protected loadingPrev: boolean = false;
protected loadingNext: boolean = false;
protected loadingPage: boolean = false;
protected constructor(params: P = {} as P, page: number = 1, pageSize: number | null = null) {
this.params = params;
@@ -139,12 +141,19 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
}
return app.store.find<T[]>(this.type, params).then((results) => {
const usedPerPage = results.payload?.meta?.perPage;
const usedTotal = results.payload?.meta?.page?.total;
/*
* If this state does not rely on a preloaded API document to know the page size,
* then there is no initial list, and therefore the page size can be taken from subsequent requests.
*/
if (!this.pageSize) {
this.pageSize = results.payload?.meta?.perPage || PaginatedListState.DEFAULT_PAGE_SIZE;
if (!this.pageSize || (usedPerPage && this.pageSize !== usedPerPage)) {
this.pageSize = usedPerPage || PaginatedListState.DEFAULT_PAGE_SIZE;
}
if (!this.totalItems || (usedTotal && this.totalItems !== usedTotal)) {
this.totalItems = usedTotal || null;
}
return results;
@@ -187,14 +196,25 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
this.clear();
return this.goto(page);
}
public goto(page: number): Promise<void> {
this.location = { page };
return this.loadPage()
if (!this.initialLoading) {
this.loadingPage = true;
}
return this.loadPage(page)
.then((results) => {
this.pages = [];
this.parseResults(this.location.page, results);
})
.finally(() => (this.initialLoading = false));
.finally(() => {
this.initialLoading = false;
this.loadingPage = false;
});
}
public getPages(): Page<T>[] {
@@ -205,7 +225,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
}
public isLoading(): boolean {
return this.initialLoading || this.loadingNext || this.loadingPrev;
return this.initialLoading || this.loadingNext || this.loadingPrev || this.loadingPage;
}
public isInitialLoading(): boolean {
return this.initialLoading;
@@ -322,18 +342,23 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
}
changeSort(sort: string) {
let currentSort: string | undefined;
if (sort === Object.keys(this.sortMap())[0]) {
currentSort = undefined;
} else {
currentSort = sort;
}
this.refreshParams(
{
...this.params,
sort: currentSort,
sort: sort,
},
1
);
}
changeFilter(key: string, value: string) {
this.refreshParams(
{
...this.params,
filter: {
...this.params.filter,
[key]: value,
},
},
1
);

View File

@@ -0,0 +1,13 @@
export default function formatAmount(size: number): string {
const units = ['K', 'M', 'B'];
for (let i = units.length - 1; i >= 0; i--) {
const decimal = Math.pow(1000, i + 1);
if (size >= decimal) {
return (size / decimal).toFixed(1).replace(/\.0$/, '') + units[i];
}
}
return size.toString();
}