From e2626547051acee129db518141c4d9fa12d5a817 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 2 Jun 2026 17:28:24 +0200 Subject: [PATCH] Unify 'mark as read' function for moderation queue Replaces the dropdown 'mark as read' interaction with a dedicated per-row button rendered next to the title, gates the 'mark all as read' button on canMarkAsRead(), and moves the JS module from Ui/Moderation to Component/Moderation. ref https://www.woltlab.com/community/thread/318268-moderationseintr%C3%A4ge-neue-eintr%C3%A4ge/ --- com.woltlab.wcf/templates/moderationList.tpl | 22 ++++++--- ts/WoltLabSuite/Core/Component/GridView.ts | 11 +++++ .../MarkAllModerationQueuesAsRead.ts | 34 +++++++++++++ .../Core/Ui/Moderation/MarkAllAsRead.ts | 26 ---------- .../WoltLabSuite/Core/Component/GridView.js | 8 ++- .../MarkAllModerationQueuesAsRead.js | 27 ++++++++++ .../Core/Ui/Moderation/MarkAllAsRead.js | 24 --------- .../gridView/AbstractGridView.class.php | 49 +++++++++++++++++++ .../system/gridView/GridViewColumn.class.php | 23 +++++++++ .../user/ModerationQueueGridView.class.php | 33 ++++++++----- .../ModerationQueueInteractions.class.php | 8 --- wcfsetup/install/files/style/ui/gridView.scss | 24 +++++++++ 12 files changed, 210 insertions(+), 79 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.ts delete mode 100644 ts/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.js delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.js diff --git a/com.woltlab.wcf/templates/moderationList.tpl b/com.woltlab.wcf/templates/moderationList.tpl index fa00b83dccc..5f288af7a4d 100644 --- a/com.woltlab.wcf/templates/moderationList.tpl +++ b/com.woltlab.wcf/templates/moderationList.tpl @@ -6,7 +6,21 @@ {/capture} {capture assign='contentInteractionButtons'} - + {if $gridView->canMarkAsRead()} + + + + {/if} {icon name='trash-can'} {lang}wcf.moderation.showDeletedContent{/lang} {/capture} @@ -16,10 +30,4 @@ {unsafe:$gridView->render()} - - {include file='footer'} diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index ddfa7b50023..78338c938f9 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -10,6 +10,7 @@ import { getRow } from "../Api/GridViews/GetRow"; import { getRows } from "../Api/GridViews/GetRows"; import { getBulkContextMenuOptions } from "../Api/Interactions/GetBulkContextMenuOptions"; +import { postObject } from "../Api/PostObject"; import DomChangeListener from "../Dom/Change/Listener"; import DomUtil from "../Dom/Util"; import { promiseMutex } from "../Helper/PromiseMutex"; @@ -104,6 +105,16 @@ export class GridView { }); } }); + + wheneverFirstSeen(`#${this.#table.id} tbody tr .gridView__row__markAsRead`, (button: HTMLButtonElement) => { + button.addEventListener( + "click", + promiseMutex(async () => { + await postObject(button.dataset.endpoint!); + button.closest("tr")?.dispatchEvent(new CustomEvent("interaction:invalidate", { bubbles: true })); + }), + ); + }); } #initEventListeners(): void { diff --git a/ts/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.ts b/ts/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.ts new file mode 100644 index 00000000000..ff24b274294 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.ts @@ -0,0 +1,34 @@ +/** + * Handles the 'mark as read' action for moderation queues. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + * @woltlabExcludeBundle tiny + */ + +import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; +import { markAllModerationQueuesAsRead } from "WoltLabSuite/Core/Api/ModerationQueues/MarkAllModerationQueuesAsRead"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; + +async function markAllAsRead(button: HTMLElement, gridView?: HTMLElement): Promise { + (await markAllModerationQueuesAsRead()).unwrap(); + + if (gridView !== undefined) { + gridView.dispatchEvent(new CustomEvent("interaction:invalidate-all")); + } + + button.remove(); + + showDefaultSuccessSnackbar(); +} + +export function setup(button: HTMLElement, gridView?: HTMLElement): void { + button.addEventListener( + "click", + promiseMutex(async () => { + await markAllAsRead(button, gridView); + }), + ); +} diff --git a/ts/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.ts b/ts/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.ts deleted file mode 100644 index c6cec9f972e..00000000000 --- a/ts/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Marks all moderation queues as read. - * - * @author Marcel Werk - * @copyright 2001-2022 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.0 - */ - -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { markAllModerationQueuesAsRead } from "WoltLabSuite/Core/Api/ModerationQueues/MarkAllModerationQueuesAsRead"; - -async function markAllAsRead(): Promise { - (await markAllModerationQueuesAsRead()).unwrap(); - - const gridViewTable = document.getElementById("wcf-system-gridView-user-ModerationQueueGridView_table")!; - gridViewTable.dispatchEvent(new CustomEvent("interaction:invalidate-all")); - - showDefaultSuccessSnackbar(); -} - -export function setup(): void { - document.querySelector(".markAllAsReadButton")?.addEventListener("click", () => { - void markAllAsRead(); - }); -} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js index 968ef2424a8..8769b823181 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ -define(["require", "exports", "tslib", "../Api/GridViews/GetRow", "../Api/GridViews/GetRows", "../Api/Interactions/GetBulkContextMenuOptions", "../Dom/Change/Listener", "../Dom/Util", "../Helper/PromiseMutex", "../Helper/Selector", "../Ui/Dropdown/Simple", "./GridView/State"], function (require, exports, tslib_1, GetRow_1, GetRows_1, GetBulkContextMenuOptions_1, Listener_1, Util_1, PromiseMutex_1, Selector_1, Simple_1, State_1) { +define(["require", "exports", "tslib", "../Api/GridViews/GetRow", "../Api/GridViews/GetRows", "../Api/Interactions/GetBulkContextMenuOptions", "../Api/PostObject", "../Dom/Change/Listener", "../Dom/Util", "../Helper/PromiseMutex", "../Helper/Selector", "../Ui/Dropdown/Simple", "./GridView/State"], function (require, exports, tslib_1, GetRow_1, GetRows_1, GetBulkContextMenuOptions_1, PostObject_1, Listener_1, Util_1, PromiseMutex_1, Selector_1, Simple_1, State_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GridView = void 0; @@ -65,6 +65,12 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRow", "../Api/GridVi }); } }); + (0, Selector_1.wheneverFirstSeen)(`#${this.#table.id} tbody tr .gridView__row__markAsRead`, (button) => { + button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + await (0, PostObject_1.postObject)(button.dataset.endpoint); + button.closest("tr")?.dispatchEvent(new CustomEvent("interaction:invalidate", { bubbles: true })); + })); + }); } #initEventListeners() { this.#table.addEventListener("interaction:invalidate-all", () => { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.js new file mode 100644 index 00000000000..791f888ce86 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Moderation/MarkAllModerationQueuesAsRead.js @@ -0,0 +1,27 @@ +/** + * Handles the 'mark as read' action for moderation queues. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + * @woltlabExcludeBundle tiny + */ +define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Api/ModerationQueues/MarkAllModerationQueuesAsRead", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, Snackbar_1, MarkAllModerationQueuesAsRead_1, PromiseMutex_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = setup; + async function markAllAsRead(button, gridView) { + (await (0, MarkAllModerationQueuesAsRead_1.markAllModerationQueuesAsRead)()).unwrap(); + if (gridView !== undefined) { + gridView.dispatchEvent(new CustomEvent("interaction:invalidate-all")); + } + button.remove(); + (0, Snackbar_1.showDefaultSuccessSnackbar)(); + } + function setup(button, gridView) { + button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + await markAllAsRead(button, gridView); + })); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.js deleted file mode 100644 index fb1c4e11253..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Moderation/MarkAllAsRead.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Marks all moderation queues as read. - * - * @author Marcel Werk - * @copyright 2001-2022 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.0 - */ -define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Api/ModerationQueues/MarkAllModerationQueuesAsRead"], function (require, exports, Snackbar_1, MarkAllModerationQueuesAsRead_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.setup = setup; - async function markAllAsRead() { - (await (0, MarkAllModerationQueuesAsRead_1.markAllModerationQueuesAsRead)()).unwrap(); - const gridViewTable = document.getElementById("wcf-system-gridView-user-ModerationQueueGridView_table"); - gridViewTable.dispatchEvent(new CustomEvent("interaction:invalidate-all")); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - function setup() { - document.querySelector(".markAllAsReadButton")?.addEventListener("click", () => { - void markAllAsRead(); - }); - } -}); diff --git a/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php b/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php index 0ff51e4f30f..ef4ef47dc21 100644 --- a/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php @@ -2,6 +2,7 @@ namespace wcf\system\gridView; +use wcf\action\ApiAction; use wcf\action\GridViewFilterAction; use wcf\data\DatabaseObject; use wcf\data\DatabaseObjectList; @@ -64,6 +65,7 @@ abstract class AbstractGridView private ?IInteractionProvider $interactionProvider = null; private ?IBulkInteractionProvider $bulkInteractionProvider = null; private InteractionContextMenuComponent $interactionContextMenuComponent; + private string $markAsReadEndpoint = ''; /** * @var array @@ -313,6 +315,10 @@ public function renderColumn(GridViewColumn $column, DatabaseObject $row): strin $value = $this->rowLink->render($value, $row, $column->isTitleColumn()); } + if ($column->hasMarkAsReadButton() && $this->rowIsNew($row)) { + $value = $this->renderMarkAsReadButton($row) . $value; + } + return $value; } @@ -952,6 +958,49 @@ public function getAvailableFilters(): array return $this->availableFilters; } + /** + * @since 6.3 + */ + public function setMarkAsReadEndpoint(string $endpoint): void + { + $this->markAsReadEndpoint = $endpoint; + } + + /** + * @since 6.3 + */ + public function renderMarkAsReadButton(DatabaseObject $object): string + { + if (!$this->markAsReadEndpoint) { + throw new \BadMethodCallException("No mark as read endpoint has been specified."); + } + + $endpoint = StringUtil::encodeHTML( + LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . + \sprintf($this->markAsReadEndpoint, $object->getObjectID()) + ); + $title = WCF::getLanguage()->get('wcf.global.button.markAsRead'); + + return << + + + HTML; + } + + /** + * @since 6.3 + */ + public function rowIsNew(DatabaseObject $row): bool + { + return false; + } + private function init(): void { if (!isset($this->objectList)) { diff --git a/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php b/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php index 1bb43a674cf..e67eae867ff 100644 --- a/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php +++ b/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php @@ -35,6 +35,7 @@ final class GridViewColumn private bool $hidden = false; private bool $unsafeDisableEncoding = false; private bool $titleColumn = false; + private bool $markAsReadButton = false; private function __construct(private readonly string $id) {} @@ -302,6 +303,28 @@ public function applyRowLink(): bool )) === 0; } + /** + * Sets whether the mark as read button is rendered in this column for unread rows. + * + * @since 6.3 + */ + public function markAsReadButton(bool $markAsReadButton = true): static + { + $this->markAsReadButton = $markAsReadButton; + + return $this; + } + + /** + * Returns true if the mark as read button is rendered in this column for unread rows. + * + * @since 6.3 + */ + public function hasMarkAsReadButton(): bool + { + return $this->markAsReadButton; + } + /** * Returns the default renderer for the rendering of columns. */ diff --git a/wcfsetup/install/files/lib/system/gridView/user/ModerationQueueGridView.class.php b/wcfsetup/install/files/lib/system/gridView/user/ModerationQueueGridView.class.php index 956e16ffdda..1880f3b0deb 100644 --- a/wcfsetup/install/files/lib/system/gridView/user/ModerationQueueGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/user/ModerationQueueGridView.class.php @@ -44,26 +44,15 @@ public function __construct() GridViewColumn::for('title') ->label('wcf.global.title') ->titleColumn() + ->markAsReadButton() ->renderer( new class extends DefaultColumnRenderer { #[\Override] public function render(mixed $value, DatabaseObject $row): string { \assert($row instanceof ViewableModerationQueue); - $title = StringUtil::encodeHTML($row->getTitle()); - if ($row->isNew()) { - $badgeLabel = WCF::getLanguage()->get('wcf.message.new'); - $badge = <<{$badgeLabel} - HTML; - } else { - $badge = ''; - } - - return <<getTitle()); } } ), @@ -216,6 +205,16 @@ public function render(mixed $value, DatabaseObject $row): string $this->setDefaultSortField("lastChangeTime"); $this->setDefaultSortOrder("DESC"); $this->addRowLink(new GridViewRowLink(isLinkableObject: true)); + $this->setMarkAsReadEndpoint('core/moderation-queues/%s/mark-as-read'); + } + + public function canMarkAsRead(): bool + { + if (!WCF::getUser()->userID) { + return false; + } + + return ModerationQueueManager::getInstance()->getUnreadModerationCount() > 0; } private function getDefinitionFilter(): SelectFilter @@ -275,4 +274,12 @@ protected function getInitializedEvent(): ModerationQueueGridViewInitialized { return new ModerationQueueGridViewInitialized($this); } + + #[\Override] + public function rowIsNew(DatabaseObject $row): bool + { + \assert($row instanceof ViewableModerationQueue); + + return $row->isNew(); + } } diff --git a/wcfsetup/install/files/lib/system/interaction/user/ModerationQueueInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/ModerationQueueInteractions.class.php index cb8d52bd4a4..c8fe5c778d2 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/ModerationQueueInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/ModerationQueueInteractions.class.php @@ -30,14 +30,6 @@ final class ModerationQueueInteractions extends AbstractInteractionProvider public function __construct() { $this->addInteractions([ - new RpcInteraction( - "mark-as-read", - "core/moderation-queues/%s/mark-as-read", - "wcf.global.button.markAsRead", - isAvailableCallback: static function (ViewableModerationQueue $queue) { - return $queue->isNew(); - } - ), new FormBuilderDialogInteraction( "assign-user", LinkHandler::getInstance()->getControllerLink(ModerationQueueAssignUserAction::class, ["id" => "%s"]), diff --git a/wcfsetup/install/files/style/ui/gridView.scss b/wcfsetup/install/files/style/ui/gridView.scss index 5a3be1c6513..c3962439a1d 100644 --- a/wcfsetup/install/files/style/ui/gridView.scss +++ b/wcfsetup/install/files/style/ui/gridView.scss @@ -117,6 +117,30 @@ } } +.gridView__row__markAsRead { + align-items: center; + display: inline-flex; + margin-right: 5px; + padding: 0 5px; + position: relative; + vertical-align: middle; + z-index: 1; +} + +.gridView__row__unread__indicator { + background-color: var(--wcfButtonPrimaryBackground); + border-radius: 50%; + display: inline-block; + height: 10px; + width: 10px; +} + +@media (hover: hover) { + .gridView__row__markAsRead:hover .gridView__row__unread__indicator { + background-color: var(--wcfButtonPrimaryBackgroundActive); + } +} + .gridView__headerColumn { background-color: var(--wcfContentContainerBackground); box-shadow: inset 0 -1px 0 0 var(--wcfContentBorder);