From: Marcel Werk Date: Mon, 9 Sep 2024 14:52:35 +0000 (+0200) Subject: Add sorting function X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=fbe528b3a36ddcac8a498972983a0279325771e5;p=GitHub%2FWoltLab%2FWCF.git Add sorting function --- diff --git a/com.woltlab.wcf/templates/shared_gridView.tpl b/com.woltlab.wcf/templates/shared_gridView.tpl index 8c63d1f10a..8957dad2ac 100644 --- a/com.woltlab.wcf/templates/shared_gridView.tpl +++ b/com.woltlab.wcf/templates/shared_gridView.tpl @@ -25,7 +25,9 @@ '{unsafe:$view->getID()|encodeJs}', '{unsafe:$view->getClassName()|encodeJS}', {$view->getPageNo()}, - '{unsafe:$view->getBaseUrl()|encodeJS}' + '{unsafe:$view->getBaseUrl()|encodeJS}', + '{unsafe:$view->getSortField()|encodeJS}', + '{unsafe:$view->getSortOrder()|encodeJS}' ); }); diff --git a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts index f84280e050..59543979c8 100644 --- a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts +++ b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts @@ -5,10 +5,17 @@ type Response = { template: string; }; -export async function getRows(gridViewClass: string, pageNo: number): Promise> { +export async function getRows( + gridViewClass: string, + pageNo: number, + sortField: string = "", + sortOrder: string = "ASC", +): Promise> { const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`); url.searchParams.set("gridView", gridViewClass); url.searchParams.set("pageNo", pageNo.toString()); + url.searchParams.set("sortField", sortField); + url.searchParams.set("sortOrder", sortOrder); let response: Response; try { diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index 73e184bd28..ffe7e65b9f 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -8,33 +8,87 @@ export class GridView { readonly #bottomPagination: WoltlabCorePaginationElement; readonly #baseUrl: string; #pageNo: number; + #sortField: string; + #sortOrder: string; - constructor(gridId: string, gridClassName: string, pageNo: number, baseUrl: string = "") { + constructor( + gridId: string, + gridClassName: string, + pageNo: number, + baseUrl: string = "", + sortField = "", + sortOrder = "ASC", + ) { this.#gridClassName = gridClassName; this.#table = document.getElementById(`${gridId}_table`) as HTMLTableElement; this.#topPagination = document.getElementById(`${gridId}_topPagination`) as WoltlabCorePaginationElement; this.#bottomPagination = document.getElementById(`${gridId}_bottomPagination`) as WoltlabCorePaginationElement; this.#pageNo = pageNo; this.#baseUrl = baseUrl; + this.#sortField = sortField; + this.#sortOrder = sortOrder; this.#initPagination(); + this.#initSorting(); } #initPagination(): void { this.#topPagination.addEventListener("switchPage", (event: CustomEvent) => { - this.#switchPage(event.detail); + void this.#switchPage(event.detail); }); this.#bottomPagination.addEventListener("switchPage", (event: CustomEvent) => { - this.#switchPage(event.detail); + void this.#switchPage(event.detail); }); } - async #switchPage(pageNo: number): Promise { + #initSorting(): void { + this.#table.querySelectorAll('th[data-sortable="1"]').forEach((element) => { + const link = document.createElement("a"); + link.role = "button"; + link.addEventListener("click", () => { + this.#sort(element.dataset.id!); + }); + + link.textContent = element.textContent; + element.innerHTML = ""; + element.append(link); + }); + + this.#renderActiveSorting(); + } + + #sort(sortField: string): void { + if (this.#sortField == sortField && this.#sortOrder == "ASC") { + this.#sortOrder = "DESC"; + } else { + this.#sortField = sortField; + this.#sortOrder = "ASC"; + } + + this.#loadRows(); + this.#renderActiveSorting(); + } + + #renderActiveSorting(): void { + this.#table.querySelectorAll('th[data-sortable="1"]').forEach((element) => { + element.classList.remove("active", "ASC", "DESC"); + + if (element.dataset.id == this.#sortField) { + element.classList.add("active", this.#sortOrder); + } + }); + } + + #switchPage(pageNo: number): void { this.#topPagination.page = pageNo; this.#bottomPagination.page = pageNo; - - const response = await getRows(this.#gridClassName, pageNo); this.#pageNo = pageNo; + + this.#loadRows(); + } + + async #loadRows(): Promise { + const response = await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder); DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.unwrap().template); this.#updateQueryString(); } @@ -50,12 +104,16 @@ export class GridView { if (this.#pageNo > 1) { parameters.push(["pageNo", this.#pageNo.toString()]); } + if (this.#sortField) { + parameters.push(["sortField", this.#sortField]); + parameters.push(["sortOrder", this.#sortOrder]); + } if (parameters.length > 0) { url.search += url.search !== "" ? "&" : "?"; url.search += new URLSearchParams(parameters).toString(); } - window.history.pushState({ name: "gridView" }, document.title, url.toString()); + window.history.pushState({}, document.title, url.toString()); } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js index 841c6b6b42..b07327102d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js @@ -2,10 +2,12 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRows = void 0; - async function getRows(gridViewClass, pageNo) { + async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC") { const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`); url.searchParams.set("gridView", gridViewClass); url.searchParams.set("pageNo", pageNo.toString()); + url.searchParams.set("sortField", sortField); + url.searchParams.set("sortOrder", sortOrder); let response; try { response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().disableLoadingIndicator().fetchAsJson()); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js index d69b702885..3178cad306 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js @@ -10,28 +10,68 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" #bottomPagination; #baseUrl; #pageNo; - constructor(gridId, gridClassName, pageNo, baseUrl = "") { + #sortField; + #sortOrder; + constructor(gridId, gridClassName, pageNo, baseUrl = "", sortField = "", sortOrder = "ASC") { this.#gridClassName = gridClassName; this.#table = document.getElementById(`${gridId}_table`); this.#topPagination = document.getElementById(`${gridId}_topPagination`); this.#bottomPagination = document.getElementById(`${gridId}_bottomPagination`); this.#pageNo = pageNo; this.#baseUrl = baseUrl; + this.#sortField = sortField; + this.#sortOrder = sortOrder; this.#initPagination(); + this.#initSorting(); } #initPagination() { this.#topPagination.addEventListener("switchPage", (event) => { - this.#switchPage(event.detail); + void this.#switchPage(event.detail); }); this.#bottomPagination.addEventListener("switchPage", (event) => { - this.#switchPage(event.detail); + void this.#switchPage(event.detail); }); } - async #switchPage(pageNo) { + #initSorting() { + this.#table.querySelectorAll('th[data-sortable="1"]').forEach((element) => { + const link = document.createElement("a"); + link.role = "button"; + link.addEventListener("click", () => { + this.#sort(element.dataset.id); + }); + link.textContent = element.textContent; + element.innerHTML = ""; + element.append(link); + }); + this.#renderActiveSorting(); + } + #sort(sortField) { + if (this.#sortField == sortField && this.#sortOrder == "ASC") { + this.#sortOrder = "DESC"; + } + else { + this.#sortField = sortField; + this.#sortOrder = "ASC"; + } + this.#loadRows(); + this.#renderActiveSorting(); + } + #renderActiveSorting() { + this.#table.querySelectorAll('th[data-sortable="1"]').forEach((element) => { + element.classList.remove("active", "ASC", "DESC"); + if (element.dataset.id == this.#sortField) { + element.classList.add("active", this.#sortOrder); + } + }); + } + #switchPage(pageNo) { this.#topPagination.page = pageNo; this.#bottomPagination.page = pageNo; - const response = await (0, GetRows_1.getRows)(this.#gridClassName, pageNo); this.#pageNo = pageNo; + this.#loadRows(); + } + async #loadRows() { + const response = await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder); Util_1.default.setInnerHtml(this.#table.querySelector("tbody"), response.unwrap().template); this.#updateQueryString(); } @@ -44,11 +84,15 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" if (this.#pageNo > 1) { parameters.push(["pageNo", this.#pageNo.toString()]); } + if (this.#sortField) { + parameters.push(["sortField", this.#sortField]); + parameters.push(["sortOrder", this.#sortOrder]); + } if (parameters.length > 0) { url.search += url.search !== "" ? "&" : "?"; url.search += new URLSearchParams(parameters).toString(); } - window.history.pushState({ name: "gridView" }, document.title, url.toString()); + window.history.pushState({}, document.title, url.toString()); } } exports.GridView = GridView; diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php index cb7f5da14d..204e1ffb83 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php @@ -21,7 +21,7 @@ final class GetRows implements IController $parameters = Helper::mapApiParameters($request, GetRowsParameters::class); if (!\is_subclass_of($parameters->gridView, AbstractGridView::class)) { - throw new UserInputException('gridView', $parameters->gridView); + throw new UserInputException('gridView', 'invalid'); } $view = new $parameters->gridView($parameters->pageNo); @@ -31,6 +31,13 @@ final class GetRows implements IController throw new PermissionDeniedException(); } + if ($parameters->sortField) { + $view->setSortField($parameters->sortField); + } + if ($parameters->sortOrder) { + $view->setSortOrder($parameters->sortOrder); + } + return new JsonResponse([ 'template' => $view->renderRows(), ]); @@ -43,6 +50,8 @@ final class GetRowsParameters public function __construct( /** @var non-empty-string */ public readonly string $gridView, - public readonly int $pageNo + public readonly int $pageNo, + public readonly string $sortField, + public readonly string $sortOrder ) {} } diff --git a/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php index 07bb3c7723..1ebf9b4021 100644 --- a/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php @@ -9,6 +9,8 @@ abstract class AbstractGridView private array $columns = []; private int $rowsPerPage = 2; private string $baseUrl = ''; + private string $sortField = ''; + private string $sortOrder = 'ASC'; public function __construct(private readonly int $pageNo = 1) { @@ -51,7 +53,11 @@ abstract class AbstractGridView foreach ($this->getColumns() as $column) { $header .= <<{$column->getLabel()} + {$column->getLabel()} EOT; } @@ -127,4 +133,40 @@ abstract class AbstractGridView { return $this->baseUrl; } + + /** + * @return GridViewColumn[] + */ + public function getSortableColumns(): array + { + return \array_filter($this->getColumns(), fn($column) => $column->isSortable()); + } + + public function setSortField(string $sortField): void + { + if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) { + throw new \InvalidArgumentException("Invalid value '{$sortField}' as sort field given."); + } + + $this->sortField = $sortField; + } + + public function setSortOrder(string $sortOrder): void + { + if ($sortOrder !== 'ASC' && $sortOrder !== 'DESC') { + throw new \InvalidArgumentException("Invalid value '{$sortOrder}' as sort order given."); + } + + $this->sortOrder = $sortOrder; + } + + public function getSortField(): string + { + return $this->sortField; + } + + public function getSortOrder(): string + { + return $this->sortOrder; + } } diff --git a/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php b/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php index 8f33a03864..bd0fb3ca6f 100644 --- a/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php @@ -12,10 +12,9 @@ final class GridViewColumn * @var IColumnRenderer[] */ private array $renderer = []; - private string $label = ''; - private static DefaultColumnRenderer $defaultRenderer; + private bool $sortable = false; private function __construct(private readonly string $id) {} @@ -72,6 +71,13 @@ final class GridViewColumn return $this; } + public function sortable(bool $sortable = true): static + { + $this->sortable = $sortable; + + return $this; + } + /** * @return IColumnRenderer[] */ @@ -90,6 +96,11 @@ final class GridViewColumn return $this->label; } + public function isSortable(): bool + { + return $this->sortable; + } + private static function getDefaultRenderer(): DefaultColumnRenderer { if (!isset(self::$defaultRenderer)) { diff --git a/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php index f6ca575906..dbc68c3ded 100644 --- a/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php @@ -22,9 +22,11 @@ final class UserRankGridView extends AbstractGridView $this->addColumns([ GridViewColumn::for('rankID') ->label('wcf.global.objectID') - ->renderer(new NumberColumnRenderer()), + ->renderer(new NumberColumnRenderer()) + ->sortable(), GridViewColumn::for('rankTitle') ->label('wcf.acp.user.rank.title') + ->sortable() ->renderer([ new class extends TitleColumnRenderer { public function render(mixed $value, mixed $context = null): string @@ -40,6 +42,7 @@ final class UserRankGridView extends AbstractGridView ]), GridViewColumn::for('rankImage') ->label('wcf.acp.user.rank.image') + ->sortable() ->renderer([ new class extends DefaultColumnRenderer { public function render(mixed $value, mixed $context = null): string @@ -52,6 +55,7 @@ final class UserRankGridView extends AbstractGridView ]), GridViewColumn::for('groupID') ->label('wcf.user.group') + ->sortable() ->renderer([ new class extends DefaultColumnRenderer { public function render(mixed $value, mixed $context = null): string @@ -62,6 +66,7 @@ final class UserRankGridView extends AbstractGridView ]), GridViewColumn::for('requiredGender') ->label('wcf.user.option.gender') + ->sortable() ->renderer([ new class extends DefaultColumnRenderer { public function render(mixed $value, mixed $context = null): string @@ -80,8 +85,11 @@ final class UserRankGridView extends AbstractGridView ]), GridViewColumn::for('requiredPoints') ->label('wcf.acp.user.rank.requiredPoints') + ->sortable() ->renderer(new NumberColumnRenderer()), ]); + + $this->setSortField('rankTitle'); } public function getRows(int $limit, int $offset = 0): array @@ -89,6 +97,9 @@ final class UserRankGridView extends AbstractGridView $list = new UserRankList(); $list->sqlLimit = $limit; $list->sqlOffset = $offset; + if ($this->getSortField()) { + $list->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder(); + } $list->readObjects(); return $list->getObjects();