Add sorting function
authorMarcel Werk <burntime@woltlab.com>
Mon, 9 Sep 2024 14:52:35 +0000 (16:52 +0200)
committerMarcel Werk <burntime@woltlab.com>
Tue, 12 Nov 2024 11:51:01 +0000 (12:51 +0100)
com.woltlab.wcf/templates/shared_gridView.tpl
ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts
ts/WoltLabSuite/Core/Component/GridView.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js
wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php
wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php
wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php
wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php

index 8c63d1f10a321c9e897d372fd61533f2243a3ecd..8957dad2acd4084e014c69b2acbf9e351c2506f4 100644 (file)
@@ -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}'
                );
        });
 </script>
index f84280e050f250c7b1dad669afd7f10f2925ea54..59543979c8f4772cd020f07d6668192ad60624f7 100644 (file)
@@ -5,10 +5,17 @@ type Response = {
   template: string;
 };
 
-export async function getRows(gridViewClass: string, pageNo: number): Promise<ApiResult<Response>> {
+export async function getRows(
+  gridViewClass: string,
+  pageNo: number,
+  sortField: string = "",
+  sortOrder: string = "ASC",
+): Promise<ApiResult<Response>> {
   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 {
index 73e184bd28ce43836374666476bcecb5ad57493f..ffe7e65b9f362bce331a0fd05b1a520427744484 100644 (file)
@@ -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<void> {
+  #initSorting(): void {
+    this.#table.querySelectorAll<HTMLTableCellElement>('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<HTMLTableCellElement>('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<void> {
+    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());
   }
 }
index 841c6b6b42d72ddbf9e7d96223527b23c91f3610..b07327102da078782955e87d9e862ca8860c9fc3 100644 (file)
@@ -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());
index d69b702885225e2ca6b93c70136d5d542b29888a..3178cad306efc7112b173b8c313deb094f566697 100644 (file)
@@ -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;
index cb7f5da14dd11bb42508282839a2edc50818b173..204e1ffb83c2c8d85f9ede76a229510d6e3e69a1 100644 (file)
@@ -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
     ) {}
 }
index 07bb3c77230d1bd780110733f17ca65261c662a4..1ebf9b402143057521700efb4a66edcc3f1c1a1c 100644 (file)
@@ -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 .= <<<EOT
-                <th class="{$column->getClasses()}">{$column->getLabel()}</th>
+                <th
+                    class="{$column->getClasses()}"
+                    data-id="{$column->getID()}"
+                    data-sortable="{$column->isSortable()}"
+                >{$column->getLabel()}</th>
             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;
+    }
 }
index 8f33a038647df4fc2e28151f9d7c8cb2bc3138df..bd0fb3ca6fb002e0d122284855558ecae03bac9f 100644 (file)
@@ -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)) {
index f6ca5759066ad38f9f4c244327802aec23d09e2a..dbc68c3ded3c9f269897c69459a97af325833b82 100644 (file)
@@ -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();