{unsafe:$column->getLabel()}
</th>
{/foreach}
+ <th></th>
</td>
</thead>
<tbody>
);
});
</script>
+ {unsafe:$view->renderActionInitialization()}
{else}
<woltlab-core-notice type="info">{lang}wcf.global.noItems{/lang}</woltlab-core-notice>
{/if}
{unsafe:$view->renderColumn($column, $row)}
</td>
{/foreach}
+ <td>
+ <div class="dropdown">
+ <button type="button" class="gridViewActions button small dropdownToggle" aria-label="{lang}wcf.global.button.more{/lang}">{icon name='ellipsis-vertical'}</button>
+
+ <ul class="dropdownMenu">
+ {foreach from=$view->getActions() item='action'}
+ <li>
+ {unsafe:$view->renderAction($action, $row)}
+ </li>
+ {/foreach}
+ </ul>
+ </div>
+ </td>
</tr>
{/foreach}
--- /dev/null
+/**
+ * Deletes an object.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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<ApiResult<[]>> {
+ try {
+ await prepareRequest(endpoint).delete().fetchAsJson();
+ } catch (e) {
+ return apiResultFromError(e);
+ }
+
+ return apiResultFromValue([]);
+}
import { getRows } from "../Api/GridViews/GetRows";
import DomUtil from "../Dom/Util";
+import UiDropdownSimple from "../Ui/Dropdown/Simple";
export class GridView {
readonly #gridClassName: string;
this.#initPagination();
this.#initSorting();
+ this.#initActions();
}
#initPagination(): void {
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 {
window.history.pushState({}, document.title, url.toString());
}
+
+ #initActions(): void {
+ this.#table.querySelectorAll<HTMLTableRowElement>("tbody tr").forEach((row) => {
+ row.querySelectorAll<HTMLElement>(".gridViewActions").forEach((element) => {
+ const dropdown = UiDropdownSimple.getDropdownMenu(element.dataset.target!)!;
+ dropdown?.querySelectorAll<HTMLButtonElement>("[data-action]").forEach((element) => {
+ element.addEventListener("click", () => {
+ row.dispatchEvent(
+ new CustomEvent("action", {
+ detail: element.dataset,
+ bubbles: true,
+ }),
+ );
+ });
+ });
+ });
+ });
+ }
}
--- /dev/null
+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<void> {
+ 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!);
+ }
+ });
+}
--- /dev/null
+/**
+ * Deletes an object.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+});
-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;
this.#sortOrder = sortOrder;
this.#initPagination();
this.#initSorting();
+ this.#initActions();
}
#initPagination() {
this.#topPagination.addEventListener("switchPage", (event) => {
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) {
}
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;
});
--- /dev/null
+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;
+});
$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);
}
);
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 = '';
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);
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] ?? '';
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;
->renderer(new NumberColumnRenderer()),
]);
+ $this->addActions([
+ new EditAction(UserRankEditForm::class),
+ new DeleteAction('core/users/ranks/%s'),
+ ]);
+
$this->setSortField('rankTitle');
}
--- /dev/null
+<?php
+
+namespace wcf\system\view\grid\action;
+
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\data\ITitledObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\view\grid\AbstractGridView;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class DeleteAction implements IGridViewAction
+{
+ public function __construct(
+ private readonly string $endpoint,
+ ) {}
+
+ #[\Override]
+ public function render(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+
+ $endpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->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 <<<HTML
+ <button type="button" data-action="delete" data-object-name="{$objectName}" data-endpoint="{$endpoint}">
+ {$label}
+ </button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(AbstractGridView $gridView): ?string
+ {
+ $id = StringUtil::encodeJS($gridView->getID());
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => {
+ setup(document.getElementById('{$id}_table'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\view\grid\action;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\view\grid\AbstractGridView;
+use wcf\system\WCF;
+
+class EditAction implements IGridViewAction
+{
+ public function __construct(
+ private readonly string $controllerClass,
+ ) {}
+
+ #[\Override]
+ public function render(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+ $href = LinkHandler::getInstance()->getControllerLink(
+ $this->controllerClass,
+ ['object' => $row]
+ );
+
+ return '<a href="' . $href . '">' . WCF::getLanguage()->get('wcf.global.button.edit') . '</a>';
+ }
+
+ #[\Override]
+ public function renderInitialization(AbstractGridView $gridView): ?string
+ {
+ return null;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\view\grid\action;
+
+use wcf\system\view\grid\AbstractGridView;
+
+interface IGridViewAction
+{
+ public function render(mixed $row): string;
+
+ public function renderInitialization(AbstractGridView $gridView): ?string;
+}