From: Marcel Werk Date: Thu, 26 Sep 2024 16:09:38 +0000 (+0200) Subject: Add context menu / row actions X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=2bababb76581b0440fe1324143ba95f412c77901;p=GitHub%2FWoltLab%2FWCF.git Add context menu / row actions --- diff --git a/com.woltlab.wcf/templates/shared_gridView.tpl b/com.woltlab.wcf/templates/shared_gridView.tpl index cd984ed440..9ec21fc755 100644 --- a/com.woltlab.wcf/templates/shared_gridView.tpl +++ b/com.woltlab.wcf/templates/shared_gridView.tpl @@ -16,6 +16,7 @@ {unsafe:$column->getLabel()} {/foreach} + @@ -40,6 +41,7 @@ ); }); + {unsafe:$view->renderActionInitialization()} {else} {lang}wcf.global.noItems{/lang} {/if} diff --git a/com.woltlab.wcf/templates/shared_gridViewRows.tpl b/com.woltlab.wcf/templates/shared_gridViewRows.tpl index 13b455f973..5b0e3ca037 100644 --- a/com.woltlab.wcf/templates/shared_gridViewRows.tpl +++ b/com.woltlab.wcf/templates/shared_gridViewRows.tpl @@ -6,5 +6,18 @@ {unsafe:$view->renderColumn($column, $row)} {/foreach} + + + {/foreach} diff --git a/ts/WoltLabSuite/Core/Api/DeleteObject.ts b/ts/WoltLabSuite/Core/Api/DeleteObject.ts new file mode 100644 index 0000000000..1ae28ef6da --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/DeleteObject.ts @@ -0,0 +1,22 @@ +/** + * Deletes an object. + * + * @author Marcel Werk + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.1 + * @woltlabExcludeBundle tiny + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "./Result"; + +export async function deleteObject(endpoint: string): Promise> { + try { + await prepareRequest(endpoint).delete().fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index 658d1138c7..40254fb7ad 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -1,5 +1,6 @@ import { getRows } from "../Api/GridViews/GetRows"; import DomUtil from "../Dom/Util"; +import UiDropdownSimple from "../Ui/Dropdown/Simple"; export class GridView { readonly #gridClassName: string; @@ -30,6 +31,7 @@ export class GridView { this.#initPagination(); this.#initSorting(); + this.#initActions(); } #initPagination(): void { @@ -91,6 +93,7 @@ export class GridView { const response = await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder); DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.unwrap().template); this.#updateQueryString(); + this.#initActions(); } #updateQueryString(): void { @@ -116,4 +119,22 @@ export class GridView { window.history.pushState({}, document.title, url.toString()); } + + #initActions(): void { + this.#table.querySelectorAll("tbody tr").forEach((row) => { + row.querySelectorAll(".gridViewActions").forEach((element) => { + const dropdown = UiDropdownSimple.getDropdownMenu(element.dataset.target!)!; + dropdown?.querySelectorAll("[data-action]").forEach((element) => { + element.addEventListener("click", () => { + row.dispatchEvent( + new CustomEvent("action", { + detail: element.dataset, + bubbles: true, + }), + ); + }); + }); + }); + }); + } } diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts new file mode 100644 index 0000000000..48275d4b43 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts @@ -0,0 +1,28 @@ +import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject"; +import { confirmationFactory } from "../../Confirmation"; +import * as UiNotification from "WoltLabSuite/Core/Ui/Notification"; + +async function handleDelete(row: HTMLTableRowElement, objectName: string, endpoint: string): Promise { + const confirmationResult = await confirmationFactory().delete(objectName); + if (!confirmationResult) { + return; + } + + const result = await deleteObject(endpoint); + if (!result.ok) { + return; + } + + row.remove(); + + // TODO: This shows a generic success message and should be replaced with a more specific message. + UiNotification.show(); +} + +export function setup(table: HTMLTableElement): void { + table.addEventListener("action", (event: CustomEvent) => { + if (event.detail.action === "delete") { + void handleDelete(event.target as HTMLTableRowElement, event.detail.objectName!, event.detail.endpoint!); + } + }); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js new file mode 100644 index 0000000000..8c35a2b549 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/DeleteObject.js @@ -0,0 +1,24 @@ +/** + * Deletes an object. + * + * @author Marcel Werk + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.1 + * @woltlabExcludeBundle tiny + */ +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; + async function deleteObject(endpoint) { + try { + await (0, Backend_1.prepareRequest)(endpoint).delete().fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } + exports.deleteObject = deleteObject; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js index 8e7876ed29..e65b94b4b0 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js @@ -1,8 +1,9 @@ -define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"], function (require, exports, tslib_1, GetRows_1, Util_1) { +define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util", "../Ui/Dropdown/Simple"], function (require, exports, tslib_1, GetRows_1, Util_1, Simple_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GridView = void 0; Util_1 = tslib_1.__importDefault(Util_1); + Simple_1 = tslib_1.__importDefault(Simple_1); class GridView { #gridClassName; #table; @@ -23,6 +24,7 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" this.#sortOrder = sortOrder; this.#initPagination(); this.#initSorting(); + this.#initActions(); } #initPagination() { this.#topPagination.addEventListener("switchPage", (event) => { @@ -68,12 +70,13 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" this.#topPagination.page = pageNo; this.#bottomPagination.page = pageNo; this.#pageNo = pageNo; - this.#loadRows(); + void 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(); + this.#initActions(); } #updateQueryString() { if (!this.#baseUrl) { @@ -94,6 +97,21 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util" } window.history.pushState({}, document.title, url.toString()); } + #initActions() { + this.#table.querySelectorAll("tbody tr").forEach((row) => { + row.querySelectorAll(".gridViewActions").forEach((element) => { + const dropdown = Simple_1.default.getDropdownMenu(element.dataset.target); + dropdown?.querySelectorAll("[data-action]").forEach((element) => { + element.addEventListener("click", () => { + row.dispatchEvent(new CustomEvent("action", { + detail: element.dataset, + bubbles: true, + })); + }); + }); + }); + }); + } } exports.GridView = GridView; }); 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 new file mode 100644 index 0000000000..a66edf2e9f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js @@ -0,0 +1,27 @@ +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; + UiNotification = tslib_1.__importStar(UiNotification); + async function handleDelete(row, objectName, endpoint) { + const confirmationResult = await (0, Confirmation_1.confirmationFactory)().delete(objectName); + if (!confirmationResult) { + return; + } + const result = await (0, DeleteObject_1.deleteObject)(endpoint); + if (!result.ok) { + return; + } + row.remove(); + // TODO: This shows a generic success message and should be replaced with a more specific message. + UiNotification.show(); + } + function setup(table) { + table.addEventListener("action", (event) => { + if (event.detail.action === "delete") { + void handleDelete(event.target, event.detail.objectName, event.detail.endpoint); + } + }); + } + exports.setup = setup; +}); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 57d5782e4c..98da328d96 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -137,6 +137,7 @@ return static function (): void { $event->register(new \wcf\system\endpoint\controller\core\gridViews\GetRows); $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession); + $event->register(new \wcf\system\endpoint\controller\core\users\ranks\DeleteUserRank); } ); 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 b3d0ea8c75..903f8ff7d1 100644 --- a/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php @@ -2,11 +2,21 @@ namespace wcf\system\view\grid; +use wcf\system\view\grid\action\IGridViewAction; use wcf\system\WCF; abstract class AbstractGridView { + /** + * @var GridViewColumn[] + */ private array $columns = []; + + /** + * @var IGridViewAction[] + */ + private array $actions = []; + private int $rowsPerPage = 20; private string $baseUrl = ''; private string $sortField = ''; @@ -54,6 +64,29 @@ abstract class AbstractGridView return null; } + /** + * @param IGridViewAction[] $columns + */ + public function addActions(array $actions): void + { + foreach ($actions as $action) { + $this->addAction($action); + } + } + + public function addAction(IGridViewAction $action): void + { + $this->actions[] = $action; + } + + /** + * @return IGridViewAction[] + */ + public function getActions(): array + { + return $this->actions; + } + public function render(): string { return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true); @@ -69,6 +102,22 @@ abstract class AbstractGridView return $column->render($this->getData($row, $column->getID()), $row); } + public function renderAction(IGridViewAction $action, mixed $row): string + { + return $action->render($row); + } + + public function renderActionInitialization(): string + { + return implode( + "\n", + \array_map( + fn($action) => $action->renderInitialization($this), + $this->getActions() + ) + ); + } + protected function getData(mixed $row, string $identifer): mixed { return $row[$identifer] ?? ''; 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 3b90e362a4..a7a091a31a 100644 --- a/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php +++ b/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php @@ -7,6 +7,8 @@ use wcf\data\DatabaseObjectList; use wcf\data\user\group\UserGroup; use wcf\data\user\rank\I18nUserRankList; use wcf\data\user\rank\UserRank; +use wcf\system\view\grid\action\DeleteAction; +use wcf\system\view\grid\action\EditAction; use wcf\system\view\grid\renderer\DefaultColumnRenderer; use wcf\system\view\grid\renderer\LinkColumnRenderer; use wcf\system\view\grid\renderer\NumberColumnRenderer; @@ -90,6 +92,11 @@ final class UserRankGridView extends DatabaseObjectListGridView ->renderer(new NumberColumnRenderer()), ]); + $this->addActions([ + new EditAction(UserRankEditForm::class), + new DeleteAction('core/users/ranks/%s'), + ]); + $this->setSortField('rankTitle'); } diff --git a/wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php new file mode 100644 index 0000000000..dd1c35d9f5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php @@ -0,0 +1,55 @@ +getControllerLink(ApiAction::class, ['id' => 'rpc']) . + \sprintf($this->endpoint, $row->getObjectID()) + ); + $label = WCF::getLanguage()->get('wcf.global.button.delete'); + if ($row instanceof ITitledObject) { + $objectName = StringUtil::encodeHTML($row->getTitle()); + } else { + $objectName = ''; + } + + return << + {$label} + + HTML; + } + + #[\Override] + public function renderInitialization(AbstractGridView $gridView): ?string + { + $id = StringUtil::encodeJS($gridView->getID()); + + return << + require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => { + setup(document.getElementById('{$id}_table')); + }); + + HTML; + } +} diff --git a/wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php new file mode 100644 index 0000000000..42761afa12 --- /dev/null +++ b/wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php @@ -0,0 +1,33 @@ +getControllerLink( + $this->controllerClass, + ['object' => $row] + ); + + return '' . WCF::getLanguage()->get('wcf.global.button.edit') . ''; + } + + #[\Override] + public function renderInitialization(AbstractGridView $gridView): ?string + { + return null; + } +} diff --git a/wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php new file mode 100644 index 0000000000..e37960a92c --- /dev/null +++ b/wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php @@ -0,0 +1,12 @@ +