From 3570d7266f8aa4a7668f4fb8979854d6ddf905a1 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Mon, 30 Sep 2024 16:08:42 +0200 Subject: [PATCH] Add filters --- com.woltlab.wcf/templates/shared_gridView.tpl | 13 ++- .../templates/shared_gridViewRows.tpl | 27 +++--- ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts | 8 ++ ts/WoltLabSuite/Core/Component/GridView.ts | 97 ++++++++++++++++++- .../js/WoltLabSuite/Core/Api/DeleteObject.js | 3 +- .../Core/Api/GridViews/GetRows.js | 10 +- .../WoltLabSuite/Core/Component/GridView.js | 76 ++++++++++++++- .../Core/Component/GridView/Action/Delete.js | 3 +- .../lib/action/GridViewFilterAction.class.php | 94 ++++++++++++++++++ .../lib/page/AbstractGridViewPage.class.php | 7 ++ .../core/gridViews/GetRows.class.php | 15 ++- .../view/grid/AbstractGridView.class.php | 58 +++++++++++ .../grid/DatabaseObjectListGridView.class.php | 14 +++ .../system/view/grid/GridViewColumn.class.php | 24 +++++ .../grid/filter/IGridViewFilter.class.php | 15 +++ .../view/grid/filter/SelectFilter.class.php | 33 +++++++ .../view/grid/filter/TextFilter.class.php | 29 ++++++ 17 files changed, 500 insertions(+), 26 deletions(-) create mode 100644 wcfsetup/install/files/lib/action/GridViewFilterAction.class.php create mode 100644 wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php create mode 100644 wcfsetup/install/files/lib/system/view/grid/filter/SelectFilter.class.php create mode 100644 wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php diff --git a/com.woltlab.wcf/templates/shared_gridView.tpl b/com.woltlab.wcf/templates/shared_gridView.tpl index 9ec21fc755..b8ea8b45ae 100644 --- a/com.woltlab.wcf/templates/shared_gridView.tpl +++ b/com.woltlab.wcf/templates/shared_gridView.tpl @@ -1,3 +1,12 @@ +{if $view->isFilterable()} + +
+ {foreach from=$view->getActiveFilters() item='value' key='key'} + + {/foreach} +
+{/if} + {if $view->countRows()}
@@ -16,7 +25,9 @@ {unsafe:$column->getLabel()} {/foreach} - + {if $view->hasActions()} + + {/if} diff --git a/com.woltlab.wcf/templates/shared_gridViewRows.tpl b/com.woltlab.wcf/templates/shared_gridViewRows.tpl index 5b0e3ca037..f494ad2251 100644 --- a/com.woltlab.wcf/templates/shared_gridViewRows.tpl +++ b/com.woltlab.wcf/templates/shared_gridViewRows.tpl @@ -6,18 +6,21 @@ {unsafe:$view->renderColumn($column, $row)} {/foreach} - - + + {/if} {/foreach} diff --git a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts index 59543979c8..6dc07920dc 100644 --- a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts +++ b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts @@ -3,6 +3,8 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; type Response = { template: string; + pages: number; + filterLabels: ArrayLike; }; export async function getRows( @@ -10,12 +12,18 @@ export async function getRows( pageNo: number, sortField: string = "", sortOrder: string = "ASC", + filters?: Map, ): 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); + if (filters) { + filters.forEach((value, key) => { + url.searchParams.set(`filters[${key}]`, value); + }); + } let response: Response; try { diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index 75e9fb3e40..6730ea4cb5 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -1,6 +1,8 @@ import { getRows } from "../Api/GridViews/GetRows"; import DomUtil from "../Dom/Util"; +import { promiseMutex } from "../Helper/PromiseMutex"; import UiDropdownSimple from "../Ui/Dropdown/Simple"; +import { dialogFactory } from "./Dialog"; export class GridView { readonly #gridClassName: string; @@ -8,11 +10,14 @@ export class GridView { readonly #topPagination: WoltlabCorePaginationElement; readonly #bottomPagination: WoltlabCorePaginationElement; readonly #baseUrl: string; + readonly #filterButton: HTMLButtonElement; + readonly #filterPills: HTMLElement; #pageNo: number; #sortField: string; #sortOrder: string; #defaultSortField: string; #defaultSortOrder: string; + #filters: Map; constructor( gridId: string, @@ -26,6 +31,8 @@ export class GridView { 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.#filterButton = document.getElementById(`${gridId}_filterButton`) as HTMLButtonElement; + this.#filterPills = document.getElementById(`${gridId}_filters`) as HTMLElement; this.#pageNo = pageNo; this.#baseUrl = baseUrl; this.#sortField = sortField; @@ -36,6 +43,7 @@ export class GridView { this.#initPagination(); this.#initSorting(); this.#initActions(); + this.#initFilters(); window.addEventListener("popstate", () => { this.#handlePopState(); @@ -98,11 +106,19 @@ export class GridView { } async #loadRows(updateQueryString: boolean = true): Promise { - const response = await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder); - DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.unwrap().template); + const response = ( + await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters) + ).unwrap(); + DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.template); + + this.#topPagination.count = response.pages; + this.#bottomPagination.count = response.pages; + if (updateQueryString) { this.#updateQueryString(); } + + this.#renderFilters(response.filterLabels); this.#initActions(); } @@ -121,6 +137,9 @@ export class GridView { parameters.push(["sortField", this.#sortField]); parameters.push(["sortOrder", this.#sortOrder]); } + this.#filters.forEach((value, key) => { + parameters.push([`filters[${key}]`, value]); + }); if (parameters.length > 0) { url.search += url.search !== "" ? "&" : "?"; @@ -148,10 +167,79 @@ export class GridView { }); } + #initFilters(): void { + if (!this.#filterButton) { + return; + } + + this.#filterButton.addEventListener( + "click", + promiseMutex(() => this.#showFilterDialog()), + ); + + if (!this.#filterPills) { + return; + } + + const filterButtons = this.#filterPills.querySelectorAll("[data-filter]"); + if (!filterButtons.length) { + return; + } + + this.#filters = new Map(); + filterButtons.forEach((button) => { + this.#filters.set(button.dataset.filter!, button.dataset.filterValue!); + button.addEventListener("click", () => { + this.#removeFilter(button.dataset.filter!); + }); + }); + } + + async #showFilterDialog(): Promise { + const url = new URL(this.#filterButton.dataset.endpoint!); + if (this.#filters) { + this.#filters.forEach((value, key) => { + url.searchParams.set(`filters[${key}]`, value); + }); + } + + const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(url.toString()); + + if (ok) { + this.#filters = new Map(Object.entries(result as ArrayLike)); + this.#switchPage(1); + } + } + + #renderFilters(labels: ArrayLike): void { + this.#filterPills.innerHTML = ""; + if (!this.#filters) { + return; + } + + this.#filters.forEach((value, key) => { + const button = document.createElement("button"); + button.type = "button"; + button.classList.add("button"); + button.innerText = labels[key]; + button.addEventListener("click", () => { + this.#removeFilter(key); + }); + + this.#filterPills.append(button); + }); + } + + #removeFilter(filter: string): void { + this.#filters.delete(filter); + this.#switchPage(1); + } + #handlePopState(): void { let pageNo = 1; this.#sortField = this.#defaultSortField; this.#sortOrder = this.#defaultSortOrder; + this.#filters = new Map(); const url = new URL(window.location.href); url.searchParams.forEach((value, key) => { @@ -167,6 +255,11 @@ export class GridView { if (key === "sortOrder") { this.#sortOrder = value; } + + const matches = key.match(/^filters\[([a-z0-9_]+)\]$/i); + if (matches) { + this.#filters.set(matches[1], value); + } }); this.#switchPage(pageNo, false); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js index 8c35a2b549..3c003f0fc9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js @@ -10,7 +10,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "./Result"], function (require, exports, Backend_1, Result_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.deleteObject = void 0; + exports.deleteObject = deleteObject; async function deleteObject(endpoint) { try { await (0, Backend_1.prepareRequest)(endpoint).delete().fetchAsJson(); @@ -20,5 +20,4 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "./Result"], fun } return (0, Result_1.apiResultFromValue)([]); } - exports.deleteObject = deleteObject; }); 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 b07327102d..7a31864e72 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js @@ -1,13 +1,18 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.getRows = void 0; - async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC") { + exports.getRows = getRows; + async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC", filters) { 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); + if (filters) { + filters.forEach((value, key) => { + url.searchParams.set(`filters[${key}]`, value); + }); + } let response; try { response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().disableLoadingIndicator().fetchAsJson()); @@ -17,5 +22,4 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu } return (0, Result_1.apiResultFromValue)(response); } - exports.getRows = getRows; }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js index d65ff5e1f0..416ef22d0c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js @@ -1,4 +1,4 @@ -define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util", "../Ui/Dropdown/Simple"], function (require, exports, tslib_1, GetRows_1, Util_1, Simple_1) { +define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util", "../Helper/PromiseMutex", "../Ui/Dropdown/Simple", "./Dialog"], function (require, exports, tslib_1, GetRows_1, Util_1, PromiseMutex_1, Simple_1, Dialog_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GridView = void 0; @@ -10,16 +10,21 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" #topPagination; #bottomPagination; #baseUrl; + #filterButton; + #filterPills; #pageNo; #sortField; #sortOrder; #defaultSortField; #defaultSortOrder; + #filters; 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.#filterButton = document.getElementById(`${gridId}_filterButton`); + this.#filterPills = document.getElementById(`${gridId}_filters`); this.#pageNo = pageNo; this.#baseUrl = baseUrl; this.#sortField = sortField; @@ -29,6 +34,7 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" this.#initPagination(); this.#initSorting(); this.#initActions(); + this.#initFilters(); window.addEventListener("popstate", () => { this.#handlePopState(); }); @@ -80,11 +86,14 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" void this.#loadRows(updateQueryString); } async #loadRows(updateQueryString = true) { - 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); + const response = (await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters)).unwrap(); + Util_1.default.setInnerHtml(this.#table.querySelector("tbody"), response.template); + this.#topPagination.count = response.pages; + this.#bottomPagination.count = response.pages; if (updateQueryString) { this.#updateQueryString(); } + this.#renderFilters(response.filterLabels); this.#initActions(); } #updateQueryString() { @@ -100,6 +109,9 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" parameters.push(["sortField", this.#sortField]); parameters.push(["sortOrder", this.#sortOrder]); } + this.#filters.forEach((value, key) => { + parameters.push([`filters[${key}]`, value]); + }); if (parameters.length > 0) { url.search += url.search !== "" ? "&" : "?"; url.search += new URLSearchParams(parameters).toString(); @@ -121,10 +133,64 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" }); }); } + #initFilters() { + if (!this.#filterButton) { + return; + } + this.#filterButton.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => this.#showFilterDialog())); + if (!this.#filterPills) { + return; + } + const filterButtons = this.#filterPills.querySelectorAll("[data-filter]"); + if (!filterButtons.length) { + return; + } + this.#filters = new Map(); + filterButtons.forEach((button) => { + this.#filters.set(button.dataset.filter, button.dataset.filterValue); + button.addEventListener("click", () => { + this.#removeFilter(button.dataset.filter); + }); + }); + } + async #showFilterDialog() { + const url = new URL(this.#filterButton.dataset.endpoint); + if (this.#filters) { + this.#filters.forEach((value, key) => { + url.searchParams.set(`filters[${key}]`, value); + }); + } + const { ok, result } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(url.toString()); + if (ok) { + this.#filters = new Map(Object.entries(result)); + this.#switchPage(1); + } + } + #renderFilters(labels) { + this.#filterPills.innerHTML = ""; + if (!this.#filters) { + return; + } + this.#filters.forEach((value, key) => { + const button = document.createElement("button"); + button.type = "button"; + button.classList.add("button"); + button.innerText = labels[key]; + button.addEventListener("click", () => { + this.#removeFilter(key); + }); + this.#filterPills.append(button); + }); + } + #removeFilter(filter) { + this.#filters.delete(filter); + this.#switchPage(1); + } #handlePopState() { let pageNo = 1; this.#sortField = this.#defaultSortField; this.#sortOrder = this.#defaultSortOrder; + this.#filters = new Map(); const url = new URL(window.location.href); url.searchParams.forEach((value, key) => { if (key === "pageNo") { @@ -137,6 +203,10 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" if (key === "sortOrder") { this.#sortOrder = value; } + const matches = key.match(/^filters\[([a-z0-9_]+)\]$/i); + if (matches) { + this.#filters.set(matches[1], value); + } }); this.#switchPage(pageNo, false); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js index a66edf2e9f..751f8ebc35 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js @@ -1,7 +1,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/DeleteObject", "../../Confirmation", "WoltLabSuite/Core/Ui/Notification"], function (require, exports, tslib_1, DeleteObject_1, Confirmation_1, UiNotification) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.setup = void 0; + exports.setup = setup; UiNotification = tslib_1.__importStar(UiNotification); async function handleDelete(row, objectName, endpoint) { const confirmationResult = await (0, Confirmation_1.confirmationFactory)().delete(objectName); @@ -23,5 +23,4 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/DeleteObject", ".. } }); } - exports.setup = setup; }); diff --git a/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php b/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php new file mode 100644 index 0000000000..f3579f8c26 --- /dev/null +++ b/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php @@ -0,0 +1,94 @@ +getQueryParams(), + <<<'EOT' + array { + gridView: string, + filters: string[] + } + EOT + ); + + if (!\is_subclass_of($parameters['gridView'], AbstractGridView::class)) { + throw new UserInputException('gridView', 'invalid'); + } + + $view = new $parameters['gridView']; + \assert($view instanceof AbstractGridView); + + if (!$view->isAccessible()) { + throw new PermissionDeniedException(); + } + + if (!$view->isFilterable()) { + throw new IllegalLinkException(); + } + + $form = $this->getForm($view, $parameters['filters']); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData()['data']; + foreach ($data as $key => $value) { + if ($value === '' || $value === null) { + unset($data[$key]); + } + } + + return new JsonResponse([ + 'result' => $data + ]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function getForm(AbstractGridView $gridView, array $values): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->get('wcf.global.filter') + ); + + foreach ($gridView->getFilterableColumns() as $column) { + $formField = $column->getFilterFormField(); + + if (isset($values[$column->getID()])) { + $formField->value($values[$column->getID()]); + } + + $form->appendChild($formField); + } + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php b/wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php index 0db6197583..0164e432cb 100644 --- a/wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php +++ b/wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php @@ -12,6 +12,7 @@ abstract class AbstractGridViewPage extends AbstractPage protected int $pageNo = 1; protected string $sortField = ''; protected string $sortOrder = ''; + protected array $filters = []; #[\Override] public function readParameters() @@ -27,6 +28,9 @@ abstract class AbstractGridViewPage extends AbstractPage if (isset($_REQUEST['sortOrder']) && ($_REQUEST['sortOrder'] === 'ASC' || $_REQUEST['sortOrder'] === 'DESC')) { $this->sortOrder = $_REQUEST['sortOrder']; } + if (isset($_REQUEST['filters']) && \is_array($_REQUEST['filters'])) { + $this->filters = $_REQUEST['filters']; + } } #[\Override] @@ -57,6 +61,9 @@ abstract class AbstractGridViewPage extends AbstractPage if ($this->sortOrder) { $this->gridView->setSortOrder($this->sortOrder); } + if ($this->filters !== []) { + $this->gridView->setActiveFilters($this->filters); + } $this->gridView->setPageNo($this->pageNo); $this->gridView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class)); } 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 9d43b27c6e..0aba1ecac1 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 @@ -39,8 +39,19 @@ final class GetRows implements IController $view->setSortOrder($parameters->sortOrder); } + if ($parameters->filters !== []) { + $view->setActiveFilters($parameters->filters); + } + + $filterLabels = []; + foreach (\array_keys($parameters->filters) as $key) { + $filterLabels[$key] = $view->getFilterLabel($key); + } + return new JsonResponse([ 'template' => $view->renderRows(), + 'pages' => $view->countPages(), + 'filterLabels' => $filterLabels, ]); } } @@ -53,6 +64,8 @@ final class GetRowsParameters public readonly string $gridView, public readonly int $pageNo, public readonly string $sortField, - public readonly string $sortOrder + public readonly string $sortOrder, + /** @var string[] */ + public readonly array $filters ) {} } 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 903f8ff7d1..ca95fc72e0 100644 --- a/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php @@ -2,6 +2,9 @@ namespace wcf\system\view\grid; +use LogicException; +use wcf\action\GridViewFilterAction; +use wcf\system\request\LinkHandler; use wcf\system\view\grid\action\IGridViewAction; use wcf\system\WCF; @@ -22,6 +25,7 @@ abstract class AbstractGridView private string $sortField = ''; private string $sortOrder = 'ASC'; private int $pageNo = 1; + private array $activeFilters = []; public function __construct() { @@ -87,6 +91,11 @@ abstract class AbstractGridView return $this->actions; } + public function hasActions(): bool + { + return $this->actions !== []; + } + public function render(): string { return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true); @@ -167,6 +176,14 @@ abstract class AbstractGridView return \array_filter($this->getColumns(), fn($column) => $column->isSortable()); } + /** + * @return GridViewColumn[] + */ + public function getFilterableColumns(): array + { + return \array_filter($this->getColumns(), fn($column) => $column->getFilter() !== null); + } + public function setSortField(string $sortField): void { if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) { @@ -214,4 +231,45 @@ abstract class AbstractGridView { $this->rowsPerPage = $rowsPerPage; } + + public function isFilterable(): bool + { + return $this->getFilterableColumns() !== []; + } + + public function getFilterActionEndpoint(): string + { + return LinkHandler::getInstance()->getControllerLink( + GridViewFilterAction::class, + ['gridView' => \get_class($this)] + ); + } + + public function setActiveFilters(array $filters): void + { + $this->activeFilters = $filters; + } + + public function getActiveFilters(): array + { + return $this->activeFilters; + } + + public function getFilterLabel(string $id): string + { + $column = $this->getColumn($id); + if (!$column) { + throw new LogicException("Unknown column '" . $id . "'."); + } + + if (!$column->getFilter()) { + throw new LogicException("Column '" . $id . "' has no filter."); + } + + if (!isset($this->activeFilters[$id])) { + throw new LogicException("No value for filter '" . $id . "' found."); + } + + return $column->getLabel() . ': ' . $column->getFilter()->renderValue($this->activeFilters[$id]); + } } diff --git a/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php index 1d8fb364e4..d1ae67b3d9 100644 --- a/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php @@ -2,6 +2,7 @@ namespace wcf\system\view\grid; +use LogicException; use wcf\data\DatabaseObject; use wcf\data\DatabaseObjectList; @@ -46,6 +47,7 @@ abstract class DatabaseObjectListGridView extends AbstractGridView $this->objectList->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder(); } } + $this->applyFilters(); } public function getObjectList(): DatabaseObjectList @@ -57,5 +59,17 @@ abstract class DatabaseObjectListGridView extends AbstractGridView return $this->objectList; } + private function applyFilters(): void + { + foreach ($this->getActiveFilters() as $key => $value) { + $column = $this->getColumn($key); + if (!$column) { + throw new LogicException("Unknown column '" . $key . "'"); + } + + $column->getFilter()->applyFilter($this->getObjectList(), $column->getID(), $value); + } + } + protected abstract function createObjectList(): DatabaseObjectList; } 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 2200deabc0..d8c699a82a 100644 --- a/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php @@ -2,6 +2,8 @@ namespace wcf\system\view\grid; +use wcf\system\form\builder\field\AbstractFormField; +use wcf\system\view\grid\filter\IGridViewFilter; use wcf\system\view\grid\renderer\DefaultColumnRenderer; use wcf\system\view\grid\renderer\IColumnRenderer; use wcf\system\WCF; @@ -16,6 +18,7 @@ final class GridViewColumn private static DefaultColumnRenderer $defaultRenderer; private bool $sortable = false; private string $sortById = ''; + private ?IGridViewFilter $filter = null; private function __construct(private readonly string $id) {} @@ -114,6 +117,27 @@ final class GridViewColumn return $this->sortById; } + public function filter(?IGridViewFilter $filter): static + { + $this->filter = $filter; + + return $this; + } + + public function getFilter(): ?IGridViewFilter + { + return $this->filter; + } + + public function getFilterFormField(): AbstractFormField + { + if ($this->getFilter() === null) { + throw new \LogicException('This column has no filter.'); + } + + return $this->getFilter()->getFormField($this->getID(), $this->getLabel()); + } + private static function getDefaultRenderer(): DefaultColumnRenderer { if (!isset(self::$defaultRenderer)) { diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php new file mode 100644 index 0000000000..aecf4cd9cc --- /dev/null +++ b/wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php @@ -0,0 +1,15 @@ +label($label) + ->options($this->options); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $id, string $value): void + { + $list->getConditionBuilder()->add("$id = ?", [$value]); + } + + #[\Override] + public function renderValue(string $value): string + { + return WCF::getLanguage()->get($this->options[$value]); + } +} diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php new file mode 100644 index 0000000000..0f4b5aed69 --- /dev/null +++ b/wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php @@ -0,0 +1,29 @@ +label($label); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $id, string $value): void + { + $list->getConditionBuilder()->add("$id LIKE ?", ['%' . $value . '%']); + } + + #[\Override] + public function renderValue(string $value): string + { + return $value; + } +} -- 2.20.1