{/if}
</th>
{/foreach}
- {if $view->hasActions()}
+ {if $view->hasInteractions()}
<th class="gridView__headerColumn gridView__actionColumn"></th>
{/if}
</td>
);
});
</script>
-{unsafe:$view->renderActionInitialization()}
+{if $view->hasInteractions()}
+ {unsafe:$view->renderInteractionInitialization()}
+{/if}
{unsafe:$view->renderColumn($column, $row)}
</td>
{/foreach}
- {if $view->hasActions()}
+ {if $view->hasInteractions()}
<td class="gridView__column gridView__actionColumn">
<div class="gridView__actionColumn__buttons">
- {foreach from=$view->getQuickActions() item='action'}
- {unsafe:$view->renderAction($action, $row)}
- {/foreach}
-
- {if $view->hasDropdownActions()}
- {hascontent}
- <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">
- {content}
- {foreach from=$view->getDropdownActions() item='action'}
- {if $action->isAvailable($row)}
- <li>
- {unsafe:$view->renderAction($action, $row)}
- </li>
- {/if}
- {/foreach}
- {/content}
- </ul>
- </div>
- {hascontentelse}
- <button type="button" disabled class="button small" aria-label="{lang}wcf.global.button.more{/lang}">
- {icon name='ellipsis-vertical'}
- </button>
- {/hascontent}
- {/if}
+ {unsafe:$view->renderQuickInteractions($row)}
+ {unsafe:$view->renderInteractionContextMenuButton($row)}
</div>
</td>
{/if}
--- /dev/null
+{if $contextMenuOptions}
+ <div class="dropdown">
+ <button type="button" class="button small dropdownToggle" aria-label="{lang}wcf.global.button.more{/lang}">
+ {icon name='ellipsis-vertical'}
+ </button>
+
+ <ul class="dropdownMenu">
+ {unsafe:$contextMenuOptions}
+ </ul>
+ </div>
+{else}
+ <button type="button" disabled class="button small" aria-label="{lang}wcf.global.button.more{/lang}">
+ {icon name='ellipsis-vertical'}
+ </button>
+{/if}
--- /dev/null
+<div class="dropdown" id="{$containerID}">
+ <button type="button" class="button dropdownToggle" aria-label="{lang}wcf.global.button.more{/lang}">
+ {icon name='ellipsis-vertical'}
+ </button>
+
+ <ul class="dropdownMenu">
+ {unsafe:$contextMenuOptions}
+ </ul>
+</div>
+
+<script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Interaction/StandaloneButton'], ({ StandaloneButton }) => {
+ new StandaloneButton(
+ document.getElementById('{unsafe:$containerID|encodeJS}'),
+ '{unsafe:$providerClassName|encodeJS}',
+ '{unsafe:$objectID|encodeJS}',
+ '{unsafe:$redirectUrl|encodeJS}'
+ );
+ });
+</script>
+
+{unsafe:$initializationCode}
--- /dev/null
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";
+
+type Response = {
+ template: string;
+};
+
+export async function getContextMenuOptions(
+ providerClassName: string,
+ objectId: number | string,
+): Promise<ApiResult<Response>> {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/interactions/context-menu-options`);
+ url.searchParams.set("provider", providerClassName);
+ url.searchParams.set("objectID", objectId.toString());
+
+ let response: Response;
+ try {
+ response = (await prepareRequest(url).get().allowCaching().disableLoadingIndicator().fetchAsJson()) as Response;
+ } catch (e) {
+ return apiResultFromError(e);
+ }
+
+ return apiResultFromValue(response);
+}
import DomChangeListener from "../Dom/Change/Listener";
import DomUtil from "../Dom/Util";
import { promiseMutex } from "../Helper/PromiseMutex";
+import { wheneverFirstSeen } from "../Helper/Selector";
import UiDropdownSimple from "../Ui/Dropdown/Simple";
import { dialogFactory } from "./Dialog";
this.#initPagination();
this.#initSorting();
- this.#initActions();
+ this.#initInteractions();
this.#initFilters();
this.#initEventListeners();
DomChangeListener.trigger();
this.#renderFilters(response.filterLabels);
- this.#initActions();
}
async #refreshRow(row: HTMLElement): Promise<void> {
window.history.pushState({}, document.title, url.toString());
}
- #initActions(): void {
- this.#table.querySelectorAll<HTMLTableRowElement>("tbody tr").forEach((row) => {
- row.querySelectorAll<HTMLElement>(".gridViewActions").forEach((element) => {
+ #initInteractions(): void {
+ wheneverFirstSeen(`#${this.#table.id} tbody tr`, (row) => {
+ row.querySelectorAll<HTMLElement>(".dropdownToggle").forEach((element) => {
let dropdown = UiDropdownSimple.getDropdownMenu(element.dataset.target!);
if (!dropdown) {
dropdown = element.closest(".dropdown")!.querySelector<HTMLElement>(".dropdownMenu")!;
}
- dropdown?.querySelectorAll<HTMLButtonElement>("[data-action]").forEach((element) => {
+ dropdown?.querySelectorAll<HTMLButtonElement>("[data-interaction]").forEach((element) => {
element.addEventListener("click", () => {
row.dispatchEvent(
- new CustomEvent("action", {
+ new CustomEvent("interaction", {
detail: element.dataset,
bubbles: true,
}),
this.#table.addEventListener("refresh", (event) => {
void this.#refreshRow(event.target as HTMLElement);
});
+
+ this.#table.addEventListener("remove", (event) => {
+ (event.target as HTMLElement).remove();
+ });
}
}
+++ /dev/null
-import { confirmationFactory } from "WoltLabSuite/Core/Component/Confirmation";
-
-export enum ConfirmationType {
- None = "None",
- SoftDelete = "SoftDelete",
- SoftDeleteWithReason = "SoftDeleteWithReason",
- Restore = "Restore",
- Delete = "Delete",
- Custom = "Custom",
-}
-
-type ResultConfirmationWithReason = {
- result: boolean;
- reason?: string;
-};
-
-export async function handleConfirmation(
- objectName: string,
- confirmationType: ConfirmationType,
- customMessage: string = "",
-): Promise<ResultConfirmationWithReason> {
- if (confirmationType == ConfirmationType.SoftDelete) {
- return await confirmationFactory().softDelete(objectName);
- }
-
- if (confirmationType == ConfirmationType.SoftDeleteWithReason) {
- return await confirmationFactory().softDelete(objectName, true);
- }
-
- if (confirmationType == ConfirmationType.Restore) {
- return {
- result: await confirmationFactory().restore(objectName ? objectName : undefined),
- };
- }
-
- if (confirmationType == ConfirmationType.Delete) {
- return {
- result: await confirmationFactory().delete(objectName ? objectName : undefined),
- };
- }
-
- if (confirmationType == ConfirmationType.Custom) {
- return {
- result: await confirmationFactory().custom(customMessage).withoutMessage(),
- };
- }
-
- return {
- result: 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 ? objectName : undefined);
- 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
-/**
- * Handles execution of DBO actions within grid views.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
- */
-
-import { dboAction } from "WoltLabSuite/Core/Ajax";
-import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
-import { ConfirmationType, handleConfirmation } from "./Confirmation";
-
-async function handleDboAction(
- row: HTMLTableRowElement,
- objectName: string,
- className: string,
- actionName: string,
- confirmationType: ConfirmationType,
- customConfirmationMessage: string = "",
-): Promise<void> {
- const confirmationResult = await handleConfirmation(objectName, confirmationType, customConfirmationMessage);
- if (!confirmationResult.result) {
- return;
- }
-
- await dboAction(actionName, className)
- .objectIds([parseInt(row.dataset.objectId!)])
- .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {})
- .dispatch();
-
- if (confirmationType == ConfirmationType.Delete) {
- row.remove();
- } else {
- row.dispatchEvent(
- new CustomEvent("refresh", {
- bubbles: true,
- }),
- );
-
- // TODO: This shows a generic success message and should be replaced with a more specific message.
- showNotification();
- }
-}
-
-export function setup(table: HTMLTableElement): void {
- table.addEventListener("action", (event: CustomEvent) => {
- if (event.detail.action === "legacy-dbo-action") {
- void handleDboAction(
- event.target as HTMLTableRowElement,
- event.detail.objectName,
- event.detail.className,
- event.detail.actionName,
- event.detail.confirmationType,
- event.detail.confirmationMessage,
- );
- }
- });
-}
+++ /dev/null
-import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject";
-import { postObject } from "WoltLabSuite/Core/Api/PostObject";
-import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
-import { ConfirmationType, handleConfirmation } from "./Confirmation";
-
-async function handleRpcAction(
- row: HTMLTableRowElement,
- objectName: string,
- endpoint: string,
- confirmationType: ConfirmationType,
- customConfirmationMessage: string = "",
-): Promise<void> {
- const confirmationResult = await handleConfirmation(objectName, confirmationType, customConfirmationMessage);
- if (!confirmationResult.result) {
- return;
- }
-
- if (confirmationType == ConfirmationType.Delete) {
- const result = await deleteObject(endpoint);
- if (!result.ok) {
- return;
- }
- } else {
- const result = await postObject(
- endpoint,
- confirmationResult.reason ? { reason: confirmationResult.reason } : undefined,
- );
- if (!result.ok) {
- return;
- }
- }
-
- if (confirmationType == ConfirmationType.Delete) {
- row.remove();
- } else {
- row.dispatchEvent(
- new CustomEvent("refresh", {
- bubbles: true,
- }),
- );
-
- // TODO: This shows a generic success message and should be replaced with a more specific message.
- showNotification();
- }
-}
-
-export function setup(table: HTMLTableElement): void {
- table.addEventListener("action", (event: CustomEvent) => {
- if (event.detail.action === "rpc") {
- void handleRpcAction(
- event.target as HTMLTableRowElement,
- event.detail.objectName,
- event.detail.endpoint,
- event.detail.confirmationType,
- event.detail.confirmationMessage,
- );
- }
- });
-}
+++ /dev/null
-import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
-import { postObject } from "WoltLabSuite/Core/Api/PostObject";
-
-async function handleToggle(checked: boolean, enableEndpoint: string, disableEndpoint: string): Promise<void> {
- await postObject(checked ? enableEndpoint : disableEndpoint);
-}
-
-export function setup(tableId: string): void {
- wheneverFirstSeen(`#${tableId} .gridView__row woltlab-core-toggle-button`, (toggleButton) => {
- toggleButton.addEventListener("change", (event: CustomEvent) => {
- void handleToggle(
- event.detail.checked as boolean,
- toggleButton.dataset.enableEndpoint!,
- toggleButton.dataset.disableEndpoint!,
- );
- });
- });
-}
--- /dev/null
+import { confirmationFactory } from "WoltLabSuite/Core/Component/Confirmation";
+
+export enum ConfirmationType {
+ None = "None",
+ SoftDelete = "SoftDelete",
+ SoftDeleteWithReason = "SoftDeleteWithReason",
+ Restore = "Restore",
+ Delete = "Delete",
+ Custom = "Custom",
+}
+
+type ResultConfirmationWithReason = {
+ result: boolean;
+ reason?: string;
+};
+
+export async function handleConfirmation(
+ objectName: string,
+ confirmationType: ConfirmationType,
+ customMessage: string = "",
+): Promise<ResultConfirmationWithReason> {
+ if (confirmationType == ConfirmationType.SoftDelete) {
+ return await confirmationFactory().softDelete(objectName);
+ }
+
+ if (confirmationType == ConfirmationType.SoftDeleteWithReason) {
+ return await confirmationFactory().softDelete(objectName, true);
+ }
+
+ if (confirmationType == ConfirmationType.Restore) {
+ return {
+ result: await confirmationFactory().restore(objectName ? objectName : undefined),
+ };
+ }
+
+ if (confirmationType == ConfirmationType.Delete) {
+ return {
+ result: await confirmationFactory().delete(objectName ? objectName : undefined),
+ };
+ }
+
+ if (confirmationType == ConfirmationType.Custom) {
+ return {
+ result: await confirmationFactory().custom(customMessage).withoutMessage(),
+ };
+ }
+
+ return {
+ result: true,
+ };
+}
--- /dev/null
+import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
+import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
+
+async function handleFormBuilderDialogAction(element: HTMLElement, endpoint: string): Promise<void> {
+ const { ok } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint);
+
+ if (!ok) {
+ return;
+ }
+
+ element.dispatchEvent(
+ new CustomEvent("refresh", {
+ bubbles: true,
+ }),
+ );
+
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ showNotification();
+}
+
+export function setup(identifier: string, container: HTMLElement): void {
+ container.addEventListener("interaction", (event: CustomEvent) => {
+ if (event.detail.interaction === identifier) {
+ void handleFormBuilderDialogAction(event.target as HTMLElement, event.detail.endpoint);
+ }
+ });
+}
--- /dev/null
+/**
+ * Handles execution of DBO actions within grid views.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
+ */
+
+import { dboAction } from "WoltLabSuite/Core/Ajax";
+import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
+import { ConfirmationType, handleConfirmation } from "./Confirmation";
+
+async function handleDboAction(
+ element: HTMLElement,
+ objectName: string,
+ className: string,
+ actionName: string,
+ confirmationType: ConfirmationType,
+ customConfirmationMessage: string = "",
+): Promise<void> {
+ const confirmationResult = await handleConfirmation(objectName, confirmationType, customConfirmationMessage);
+ if (!confirmationResult.result) {
+ return;
+ }
+
+ await dboAction(actionName, className)
+ .objectIds([parseInt(element.dataset.objectId!)])
+ .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {})
+ .dispatch();
+
+ if (confirmationType == ConfirmationType.Delete) {
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ showNotification(undefined, () => {
+ element.dispatchEvent(
+ new CustomEvent("remove", {
+ bubbles: true,
+ }),
+ );
+ });
+ } else {
+ element.dispatchEvent(
+ new CustomEvent("refresh", {
+ bubbles: true,
+ }),
+ );
+
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ showNotification();
+ }
+}
+
+export function setup(identifier: string, container: HTMLElement): void {
+ container.addEventListener("interaction", (event: CustomEvent) => {
+ if (event.detail.interaction === identifier) {
+ void handleDboAction(
+ event.target as HTMLElement,
+ event.detail.objectName,
+ event.detail.className,
+ event.detail.actionName,
+ event.detail.confirmationType,
+ event.detail.confirmationMessage,
+ );
+ }
+ });
+}
--- /dev/null
+import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject";
+import { postObject } from "WoltLabSuite/Core/Api/PostObject";
+import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
+import { ConfirmationType, handleConfirmation } from "./Confirmation";
+
+async function handleRpcInteraction(
+ element: HTMLElement,
+ objectName: string,
+ endpoint: string,
+ confirmationType: ConfirmationType,
+ customConfirmationMessage: string = "",
+): Promise<void> {
+ const confirmationResult = await handleConfirmation(objectName, confirmationType, customConfirmationMessage);
+ if (!confirmationResult.result) {
+ return;
+ }
+
+ if (confirmationType == ConfirmationType.Delete) {
+ const result = await deleteObject(endpoint);
+ if (!result.ok) {
+ return;
+ }
+ } else {
+ const result = await postObject(
+ endpoint,
+ confirmationResult.reason ? { reason: confirmationResult.reason } : undefined,
+ );
+ if (!result.ok) {
+ return;
+ }
+ }
+
+ if (confirmationType === ConfirmationType.Delete) {
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ showNotification(undefined, () => {
+ element.dispatchEvent(
+ new CustomEvent("remove", {
+ bubbles: true,
+ }),
+ );
+ });
+ } else {
+ element.dispatchEvent(
+ new CustomEvent("refresh", {
+ bubbles: true,
+ }),
+ );
+
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ showNotification();
+ }
+}
+
+export function setup(identifier: string, container: HTMLElement): void {
+ container.addEventListener("interaction", (event: CustomEvent) => {
+ if (event.detail.interaction === identifier) {
+ void handleRpcInteraction(
+ event.target as HTMLElement,
+ event.detail.objectName,
+ event.detail.endpoint,
+ event.detail.confirmationType,
+ event.detail.confirmationMessage,
+ );
+ }
+ });
+}
--- /dev/null
+import { getContextMenuOptions } from "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions";
+import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple";
+
+export class StandaloneButton {
+ #container: HTMLElement;
+ #providerClassName: string;
+ #objectId: string | number;
+ #redirectUrl: string;
+
+ constructor(container: HTMLElement, providerClassName: string, objectId: string | number, redirectUrl: string) {
+ this.#container = container;
+ this.#providerClassName = providerClassName;
+ this.#objectId = objectId;
+ this.#redirectUrl = redirectUrl;
+
+ this.#initInteractions();
+ this.#initEventListeners();
+ }
+
+ async #refreshContextMenu(): Promise<void> {
+ const response = (await getContextMenuOptions(this.#providerClassName, this.#objectId)).unwrap();
+
+ const dropdown = this.#getDropdownMenu();
+ if (!dropdown) {
+ return;
+ }
+
+ dropdown.innerHTML = response.template;
+
+ this.#initInteractions();
+ }
+
+ #getDropdownMenu(): HTMLElement | undefined {
+ const button = this.#container.querySelector<HTMLButtonElement>(".dropdownToggle");
+ if (!button) {
+ return undefined;
+ }
+
+ let dropdown = UiDropdownSimple.getDropdownMenu(button.dataset.target!);
+ if (!dropdown) {
+ dropdown = button.closest(".dropdown")!.querySelector<HTMLElement>(".dropdownMenu")!;
+ }
+
+ return dropdown;
+ }
+
+ #initInteractions(): void {
+ this.#getDropdownMenu()
+ ?.querySelectorAll<HTMLButtonElement>("[data-interaction]")
+ .forEach((element) => {
+ element.addEventListener("click", () => {
+ this.#container.dispatchEvent(
+ new CustomEvent("interaction", {
+ detail: element.dataset,
+ bubbles: true,
+ }),
+ );
+ });
+ });
+ }
+
+ #initEventListeners(): void {
+ this.#container.addEventListener("refresh", () => {
+ void this.#refreshContextMenu();
+ });
+
+ this.#container.addEventListener("remove", () => {
+ window.location.href = this.#redirectUrl;
+ });
+ }
+}
--- /dev/null
+import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { postObject } from "WoltLabSuite/Core/Api/PostObject";
+
+async function handleToggle(checked: boolean, enableEndpoint: string, disableEndpoint: string): Promise<void> {
+ await postObject(checked ? enableEndpoint : disableEndpoint);
+}
+
+export function setup(identifier: string, container: HTMLElement): void {
+ wheneverFirstSeen(`#${container.id} [data-interaction="${identifier}"]`, (toggleButton) => {
+ toggleButton.addEventListener("change", (event: CustomEvent) => {
+ void handleToggle(
+ event.detail.checked as boolean,
+ toggleButton.dataset.enableEndpoint!,
+ toggleButton.dataset.disableEndpoint!,
+ );
+ });
+ });
+}
<nav class="contentHeaderNavigation">
<ul>
- <li><a href="{link controller='UserRankList'}{/link}" class="button">{icon name='list'} <span>{lang}wcf.acp.menu.link.user.rank.list{/lang}</span></a></li>
-
+ {if $action == 'edit'}
+ <li>
+ {unsafe:$interactionContextMenu->render()}
+ </li>
+ {/if}
{event name='contentHeaderNavigation'}
</ul>
</nav>
--- /dev/null
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.getContextMenuOptions = getContextMenuOptions;
+ async function getContextMenuOptions(providerClassName, objectId) {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/interactions/context-menu-options`);
+ url.searchParams.set("provider", providerClassName);
+ url.searchParams.set("objectID", objectId.toString());
+ let response;
+ try {
+ response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().disableLoadingIndicator().fetchAsJson());
+ }
+ catch (e) {
+ return (0, Result_1.apiResultFromError)(e);
+ }
+ return (0, Result_1.apiResultFromValue)(response);
+ }
+});
-define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridviews/GetRows", "../Dom/Change/Listener", "../Dom/Util", "../Helper/PromiseMutex", "../Ui/Dropdown/Simple", "./Dialog"], function (require, exports, tslib_1, GetRow_1, GetRows_1, Listener_1, Util_1, PromiseMutex_1, Simple_1, Dialog_1) {
+define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridviews/GetRows", "../Dom/Change/Listener", "../Dom/Util", "../Helper/PromiseMutex", "../Helper/Selector", "../Ui/Dropdown/Simple", "./Dialog"], function (require, exports, tslib_1, GetRow_1, GetRows_1, Listener_1, Util_1, PromiseMutex_1, Selector_1, Simple_1, Dialog_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GridView = void 0;
this.#gridViewParameters = gridViewParameters;
this.#initPagination();
this.#initSorting();
- this.#initActions();
+ this.#initInteractions();
this.#initFilters();
this.#initEventListeners();
window.addEventListener("popstate", () => {
}
Listener_1.default.trigger();
this.#renderFilters(response.filterLabels);
- this.#initActions();
}
async #refreshRow(row) {
const response = (await (0, GetRow_1.getRow)(this.#gridClassName, row.dataset.objectId)).unwrap();
}
window.history.pushState({}, document.title, url.toString());
}
- #initActions() {
- this.#table.querySelectorAll("tbody tr").forEach((row) => {
- row.querySelectorAll(".gridViewActions").forEach((element) => {
+ #initInteractions() {
+ (0, Selector_1.wheneverFirstSeen)(`#${this.#table.id} tbody tr`, (row) => {
+ row.querySelectorAll(".dropdownToggle").forEach((element) => {
let dropdown = Simple_1.default.getDropdownMenu(element.dataset.target);
if (!dropdown) {
dropdown = element.closest(".dropdown").querySelector(".dropdownMenu");
}
- dropdown?.querySelectorAll("[data-action]").forEach((element) => {
+ dropdown?.querySelectorAll("[data-interaction]").forEach((element) => {
element.addEventListener("click", () => {
- row.dispatchEvent(new CustomEvent("action", {
+ row.dispatchEvent(new CustomEvent("interaction", {
detail: element.dataset,
bubbles: true,
}));
this.#table.addEventListener("refresh", (event) => {
void this.#refreshRow(event.target);
});
+ this.#table.addEventListener("remove", (event) => {
+ event.target.remove();
+ });
}
}
exports.GridView = GridView;
+++ /dev/null
-define(["require", "exports", "WoltLabSuite/Core/Component/Confirmation"], function (require, exports, Confirmation_1) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.ConfirmationType = void 0;
- exports.handleConfirmation = handleConfirmation;
- var ConfirmationType;
- (function (ConfirmationType) {
- ConfirmationType["None"] = "None";
- ConfirmationType["SoftDelete"] = "SoftDelete";
- ConfirmationType["SoftDeleteWithReason"] = "SoftDeleteWithReason";
- ConfirmationType["Restore"] = "Restore";
- ConfirmationType["Delete"] = "Delete";
- ConfirmationType["Custom"] = "Custom";
- })(ConfirmationType || (exports.ConfirmationType = ConfirmationType = {}));
- async function handleConfirmation(objectName, confirmationType, customMessage = "") {
- if (confirmationType == ConfirmationType.SoftDelete) {
- return await (0, Confirmation_1.confirmationFactory)().softDelete(objectName);
- }
- if (confirmationType == ConfirmationType.SoftDeleteWithReason) {
- return await (0, Confirmation_1.confirmationFactory)().softDelete(objectName, true);
- }
- if (confirmationType == ConfirmationType.Restore) {
- return {
- result: await (0, Confirmation_1.confirmationFactory)().restore(objectName ? objectName : undefined),
- };
- }
- if (confirmationType == ConfirmationType.Delete) {
- return {
- result: await (0, Confirmation_1.confirmationFactory)().delete(objectName ? objectName : undefined),
- };
- }
- if (confirmationType == ConfirmationType.Custom) {
- return {
- result: await (0, Confirmation_1.confirmationFactory)().custom(customMessage).withoutMessage(),
- };
- }
- return {
- result: true,
- };
- }
-});
+++ /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 = setup;
- UiNotification = tslib_1.__importStar(UiNotification);
- async function handleDelete(row, objectName, endpoint) {
- const confirmationResult = await (0, Confirmation_1.confirmationFactory)().delete(objectName ? objectName : undefined);
- 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);
- }
- });
- }
-});
+++ /dev/null
-/**
- * Handles execution of DBO actions within grid views.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
- */
-define(["require", "exports", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Ui/Notification", "./Confirmation"], function (require, exports, Ajax_1, Notification_1, Confirmation_1) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.setup = setup;
- async function handleDboAction(row, objectName, className, actionName, confirmationType, customConfirmationMessage = "") {
- const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage);
- if (!confirmationResult.result) {
- return;
- }
- await (0, Ajax_1.dboAction)(actionName, className)
- .objectIds([parseInt(row.dataset.objectId)])
- .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {})
- .dispatch();
- if (confirmationType == Confirmation_1.ConfirmationType.Delete) {
- row.remove();
- }
- else {
- row.dispatchEvent(new CustomEvent("refresh", {
- bubbles: true,
- }));
- // TODO: This shows a generic success message and should be replaced with a more specific message.
- (0, Notification_1.show)();
- }
- }
- function setup(table) {
- table.addEventListener("action", (event) => {
- if (event.detail.action === "legacy-dbo-action") {
- void handleDboAction(event.target, event.detail.objectName, event.detail.className, event.detail.actionName, event.detail.confirmationType, event.detail.confirmationMessage);
- }
- });
- }
-});
+++ /dev/null
-define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuite/Core/Api/PostObject", "WoltLabSuite/Core/Ui/Notification", "./Confirmation"], function (require, exports, DeleteObject_1, PostObject_1, Notification_1, Confirmation_1) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.setup = setup;
- async function handleRpcAction(row, objectName, endpoint, confirmationType, customConfirmationMessage = "") {
- const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage);
- if (!confirmationResult.result) {
- return;
- }
- if (confirmationType == Confirmation_1.ConfirmationType.Delete) {
- const result = await (0, DeleteObject_1.deleteObject)(endpoint);
- if (!result.ok) {
- return;
- }
- }
- else {
- const result = await (0, PostObject_1.postObject)(endpoint, confirmationResult.reason ? { reason: confirmationResult.reason } : undefined);
- if (!result.ok) {
- return;
- }
- }
- if (confirmationType == Confirmation_1.ConfirmationType.Delete) {
- row.remove();
- }
- else {
- row.dispatchEvent(new CustomEvent("refresh", {
- bubbles: true,
- }));
- // TODO: This shows a generic success message and should be replaced with a more specific message.
- (0, Notification_1.show)();
- }
- }
- function setup(table) {
- table.addEventListener("action", (event) => {
- if (event.detail.action === "rpc") {
- void handleRpcAction(event.target, event.detail.objectName, event.detail.endpoint, event.detail.confirmationType, event.detail.confirmationMessage);
- }
- });
- }
-});
+++ /dev/null
-define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/PostObject"], function (require, exports, Selector_1, PostObject_1) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.setup = setup;
- async function handleToggle(checked, enableEndpoint, disableEndpoint) {
- await (0, PostObject_1.postObject)(checked ? enableEndpoint : disableEndpoint);
- }
- function setup(tableId) {
- (0, Selector_1.wheneverFirstSeen)(`#${tableId} .gridView__row woltlab-core-toggle-button`, (toggleButton) => {
- toggleButton.addEventListener("change", (event) => {
- void handleToggle(event.detail.checked, toggleButton.dataset.enableEndpoint, toggleButton.dataset.disableEndpoint);
- });
- });
- }
-});
--- /dev/null
+define(["require", "exports", "WoltLabSuite/Core/Component/Confirmation"], function (require, exports, Confirmation_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.ConfirmationType = void 0;
+ exports.handleConfirmation = handleConfirmation;
+ var ConfirmationType;
+ (function (ConfirmationType) {
+ ConfirmationType["None"] = "None";
+ ConfirmationType["SoftDelete"] = "SoftDelete";
+ ConfirmationType["SoftDeleteWithReason"] = "SoftDeleteWithReason";
+ ConfirmationType["Restore"] = "Restore";
+ ConfirmationType["Delete"] = "Delete";
+ ConfirmationType["Custom"] = "Custom";
+ })(ConfirmationType || (exports.ConfirmationType = ConfirmationType = {}));
+ async function handleConfirmation(objectName, confirmationType, customMessage = "") {
+ if (confirmationType == ConfirmationType.SoftDelete) {
+ return await (0, Confirmation_1.confirmationFactory)().softDelete(objectName);
+ }
+ if (confirmationType == ConfirmationType.SoftDeleteWithReason) {
+ return await (0, Confirmation_1.confirmationFactory)().softDelete(objectName, true);
+ }
+ if (confirmationType == ConfirmationType.Restore) {
+ return {
+ result: await (0, Confirmation_1.confirmationFactory)().restore(objectName ? objectName : undefined),
+ };
+ }
+ if (confirmationType == ConfirmationType.Delete) {
+ return {
+ result: await (0, Confirmation_1.confirmationFactory)().delete(objectName ? objectName : undefined),
+ };
+ }
+ if (confirmationType == ConfirmationType.Custom) {
+ return {
+ result: await (0, Confirmation_1.confirmationFactory)().custom(customMessage).withoutMessage(),
+ };
+ }
+ return {
+ result: true,
+ };
+ }
+});
--- /dev/null
+define(["require", "exports", "WoltLabSuite/Core/Ui/Notification", "WoltLabSuite/Core/Component/Dialog"], function (require, exports, Notification_1, Dialog_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ async function handleFormBuilderDialogAction(element, endpoint) {
+ const { ok } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(endpoint);
+ if (!ok) {
+ return;
+ }
+ element.dispatchEvent(new CustomEvent("refresh", {
+ bubbles: true,
+ }));
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ (0, Notification_1.show)();
+ }
+ function setup(identifier, container) {
+ container.addEventListener("interaction", (event) => {
+ if (event.detail.interaction === identifier) {
+ void handleFormBuilderDialogAction(event.target, event.detail.endpoint);
+ }
+ });
+ }
+});
--- /dev/null
+/**
+ * Handles execution of DBO actions within grid views.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
+ */
+define(["require", "exports", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Ui/Notification", "./Confirmation"], function (require, exports, Ajax_1, Notification_1, Confirmation_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ async function handleDboAction(element, objectName, className, actionName, confirmationType, customConfirmationMessage = "") {
+ const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage);
+ if (!confirmationResult.result) {
+ return;
+ }
+ await (0, Ajax_1.dboAction)(actionName, className)
+ .objectIds([parseInt(element.dataset.objectId)])
+ .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {})
+ .dispatch();
+ if (confirmationType == Confirmation_1.ConfirmationType.Delete) {
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ (0, Notification_1.show)(undefined, () => {
+ element.dispatchEvent(new CustomEvent("remove", {
+ bubbles: true,
+ }));
+ });
+ }
+ else {
+ element.dispatchEvent(new CustomEvent("refresh", {
+ bubbles: true,
+ }));
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ (0, Notification_1.show)();
+ }
+ }
+ function setup(identifier, container) {
+ container.addEventListener("interaction", (event) => {
+ if (event.detail.interaction === identifier) {
+ void handleDboAction(event.target, event.detail.objectName, event.detail.className, event.detail.actionName, event.detail.confirmationType, event.detail.confirmationMessage);
+ }
+ });
+ }
+});
--- /dev/null
+define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuite/Core/Api/PostObject", "WoltLabSuite/Core/Ui/Notification", "./Confirmation"], function (require, exports, DeleteObject_1, PostObject_1, Notification_1, Confirmation_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ async function handleRpcInteraction(element, objectName, endpoint, confirmationType, customConfirmationMessage = "") {
+ const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage);
+ if (!confirmationResult.result) {
+ return;
+ }
+ if (confirmationType == Confirmation_1.ConfirmationType.Delete) {
+ const result = await (0, DeleteObject_1.deleteObject)(endpoint);
+ if (!result.ok) {
+ return;
+ }
+ }
+ else {
+ const result = await (0, PostObject_1.postObject)(endpoint, confirmationResult.reason ? { reason: confirmationResult.reason } : undefined);
+ if (!result.ok) {
+ return;
+ }
+ }
+ if (confirmationType === Confirmation_1.ConfirmationType.Delete) {
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ (0, Notification_1.show)(undefined, () => {
+ element.dispatchEvent(new CustomEvent("remove", {
+ bubbles: true,
+ }));
+ });
+ }
+ else {
+ element.dispatchEvent(new CustomEvent("refresh", {
+ bubbles: true,
+ }));
+ // TODO: This shows a generic success message and should be replaced with a more specific message.
+ (0, Notification_1.show)();
+ }
+ }
+ function setup(identifier, container) {
+ container.addEventListener("interaction", (event) => {
+ if (event.detail.interaction === identifier) {
+ void handleRpcInteraction(event.target, event.detail.objectName, event.detail.endpoint, event.detail.confirmationType, event.detail.confirmationMessage);
+ }
+ });
+ }
+});
--- /dev/null
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions", "WoltLabSuite/Core/Ui/Dropdown/Simple"], function (require, exports, tslib_1, GetContextMenuOptions_1, Simple_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.StandaloneButton = void 0;
+ Simple_1 = tslib_1.__importDefault(Simple_1);
+ class StandaloneButton {
+ #container;
+ #providerClassName;
+ #objectId;
+ #redirectUrl;
+ constructor(container, providerClassName, objectId, redirectUrl) {
+ this.#container = container;
+ this.#providerClassName = providerClassName;
+ this.#objectId = objectId;
+ this.#redirectUrl = redirectUrl;
+ this.#initInteractions();
+ this.#initEventListeners();
+ }
+ async #refreshContextMenu() {
+ const response = (await (0, GetContextMenuOptions_1.getContextMenuOptions)(this.#providerClassName, this.#objectId)).unwrap();
+ const dropdown = this.#getDropdownMenu();
+ if (!dropdown) {
+ return;
+ }
+ dropdown.innerHTML = response.template;
+ this.#initInteractions();
+ }
+ #getDropdownMenu() {
+ const button = this.#container.querySelector(".dropdownToggle");
+ if (!button) {
+ return undefined;
+ }
+ let dropdown = Simple_1.default.getDropdownMenu(button.dataset.target);
+ if (!dropdown) {
+ dropdown = button.closest(".dropdown").querySelector(".dropdownMenu");
+ }
+ return dropdown;
+ }
+ #initInteractions() {
+ this.#getDropdownMenu()
+ ?.querySelectorAll("[data-interaction]")
+ .forEach((element) => {
+ element.addEventListener("click", () => {
+ this.#container.dispatchEvent(new CustomEvent("interaction", {
+ detail: element.dataset,
+ bubbles: true,
+ }));
+ });
+ });
+ }
+ #initEventListeners() {
+ this.#container.addEventListener("refresh", () => {
+ void this.#refreshContextMenu();
+ });
+ this.#container.addEventListener("remove", () => {
+ window.location.href = this.#redirectUrl;
+ });
+ }
+ }
+ exports.StandaloneButton = StandaloneButton;
+});
--- /dev/null
+define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/PostObject"], function (require, exports, Selector_1, PostObject_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ async function handleToggle(checked, enableEndpoint, disableEndpoint) {
+ await (0, PostObject_1.postObject)(checked ? enableEndpoint : disableEndpoint);
+ }
+ function setup(identifier, container) {
+ (0, Selector_1.wheneverFirstSeen)(`#${container.id} [data-interaction="${identifier}"]`, (toggleButton) => {
+ toggleButton.addEventListener("change", (event) => {
+ void handleToggle(event.detail.checked, toggleButton.dataset.enableEndpoint, toggleButton.dataset.disableEndpoint);
+ });
+ });
+ }
+});
namespace wcf\acp\form;
+use wcf\acp\page\UserRankListPage;
use wcf\data\user\rank\UserRank;
use wcf\data\user\rank\UserRankAction;
use wcf\form\AbstractForm;
use wcf\system\exception\IllegalLinkException;
use wcf\system\file\upload\UploadHandler;
+use wcf\system\interaction\admin\UserRankInteractions;
+use wcf\system\interaction\StandaloneInteractionContextMenuView;
use wcf\system\language\I18nHandler;
+use wcf\system\request\LinkHandler;
use wcf\system\WCF;
/**
'rankID' => $this->rankID,
'rank' => $this->rank,
'action' => 'edit',
+ 'interactionContextMenu' => new StandaloneInteractionContextMenuView(
+ new UserRankInteractions(),
+ $this->rank,
+ LinkHandler::getInstance()->getControllerLink(UserRankListPage::class)
+ ),
]);
}
}
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\ACPSessionLogGridView;
+use wcf\system\gridView\admin\ACPSessionLogGridView;
/**
* Shows a list of logged sessions.
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\CronjobLogGridView;
+use wcf\system\gridView\admin\CronjobLogGridView;
/**
* Shows cronjob log information.
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\ExceptionLogGridView;
+use wcf\system\gridView\admin\ExceptionLogGridView;
use wcf\system\registry\RegistryHandler;
/**
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\ModificationLogGridView;
+use wcf\system\gridView\admin\ModificationLogGridView;
/**
* Shows a list of modification log items.
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\UserOptionGridView;
+use wcf\system\gridView\admin\UserOptionGridView;
/**
* Shows a list of the installed user options.
use wcf\page\AbstractGridViewPage;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\UserRankGridView;
+use wcf\system\gridView\admin\UserRankGridView;
/**
* Lists available user ranks.
$event->register(new \wcf\system\endpoint\controller\core\users\options\DisableOption);
$event->register(new \wcf\system\endpoint\controller\core\users\options\EnableOption);
$event->register(new \wcf\system\endpoint\controller\core\users\ranks\DeleteUserRank);
+ $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions());
}
);
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\ACPSessionLogGridView;
-
-/**
- * Indicates that the acp session log grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ACPSessionLogGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly ACPSessionLogGridView $gridView) {}
-}
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\CronjobLogGridView;
-
-/**
- * Indicates that the cronjob log grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class CronjobLogGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly CronjobLogGridView $gridView) {}
-}
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\ExceptionLogGridView;
-
-/**
- * Indicates that the exception log grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ExceptionLogGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly ExceptionLogGridView $gridView) {}
-}
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\ModificationLogGridView;
-
-/**
- * Indicates that the modification log grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ModificationLogGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly ModificationLogGridView $gridView) {}
-}
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\UserOptionGridView;
-
-/**
- * Indicates that the user option grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class UserOptionGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly UserOptionGridView $gridView) {}
-}
+++ /dev/null
-<?php
-
-namespace wcf\event\gridView;
-
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\UserRankGridView;
-
-/**
- * Indicates that the user rank grid view has been initialized.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class UserRankGridViewInitialized implements IPsr14Event
-{
- public function __construct(public readonly UserRankGridView $gridView) {}
-}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\ACPSessionLogGridView;
+
+/**
+ * Indicates that the acp session log grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ACPSessionLogGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly ACPSessionLogGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\CronjobLogGridView;
+
+/**
+ * Indicates that the cronjob log grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class CronjobLogGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly CronjobLogGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\ExceptionLogGridView;
+
+/**
+ * Indicates that the exception log grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ExceptionLogGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly ExceptionLogGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\ModificationLogGridView;
+
+/**
+ * Indicates that the modification log grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ModificationLogGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly ModificationLogGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\UserOptionGridView;
+
+/**
+ * Indicates that the user option grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserOptionGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly UserOptionGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\gridView\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\admin\UserRankGridView;
+
+/**
+ * Indicates that the user rank grid view has been initialized.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserRankGridViewInitialized implements IPsr14Event
+{
+ public function __construct(public readonly UserRankGridView $gridView) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\interaction\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\interaction\admin\UserOptionInteractions;
+
+/**
+ * Indicates that the provider for user option interactions is collecting interactions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserOptionInteractionCollecting implements IPsr14Event
+{
+ public function __construct(public readonly UserOptionInteractions $provider) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\event\interaction\admin;
+
+use wcf\event\IPsr14Event;
+use wcf\system\interaction\admin\UserRankInteractions;
+
+/**
+ * Indicates that the provider for user rank interactions is collecting interactions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserRankInteractionCollecting implements IPsr14Event
+{
+ public function __construct(public readonly UserRankInteractions $provider) {}
+}
--- /dev/null
+<?php
+
+namespace wcf\system\endpoint\controller\core\interactions;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\DatabaseObject;
+use wcf\http\Helper;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\UserInputException;
+use wcf\system\interaction\IInteractionProvider;
+use wcf\system\interaction\InteractionContextMenuView;
+use wcf\system\interaction\StandaloneInteractionContextMenuView;
+
+/**
+ * Retrieves the options for an interaction context menu.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+#[GetRequest('/core/interactions/context-menu-options')]
+final class GetContextMenuOptions implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $parameters = Helper::mapApiParameters($request, GetContextMenuOptionsParameters::class);
+
+ if (!\is_subclass_of($parameters->provider, IInteractionProvider::class)) {
+ throw new UserInputException('provider', 'invalid');
+ }
+
+ $provider = new $parameters->provider();
+ \assert($provider instanceof IInteractionProvider);
+
+ $object = new ($provider->getObjectClassName())($parameters->objectID);
+ \assert($object instanceof DatabaseObject);
+
+ $view = new InteractionContextMenuView($provider);
+
+ return new JsonResponse([
+ 'template' => $view->renderContextMenuOptions($object),
+ ]);
+ }
+}
+
+/** @internal */
+final class GetContextMenuOptionsParameters
+{
+ public function __construct(
+ /** @var non-empty-string */
+ public readonly string $provider,
+ public readonly int|string $objectID,
+ ) {}
+}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use wcf\acp\form\UserEditForm;
-use wcf\acp\page\ACPSessionLogPage;
-use wcf\data\acp\session\log\ACPSessionLogList;
-use wcf\data\DatabaseObjectList;
-use wcf\event\gridView\ACPSessionLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\filter\IpAddressFilter;
-use wcf\system\gridView\filter\TextFilter;
-use wcf\system\gridView\filter\TimeFilter;
-use wcf\system\gridView\filter\UserFilter;
-use wcf\system\gridView\renderer\IpAddressColumnRenderer;
-use wcf\system\gridView\renderer\NumberColumnRenderer;
-use wcf\system\gridView\renderer\TimeColumnRenderer;
-use wcf\system\gridView\renderer\TruncatedTextColumnRenderer;
-use wcf\system\gridView\renderer\UserLinkColumnRenderer;
-use wcf\system\WCF;
-
-/**
- * Grid view for the list of logged admin session.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ACPSessionLogGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('sessionLogID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('userID')
- ->label('wcf.user.username')
- ->sortable(true, 'user_table.username')
- ->titleColumn()
- ->renderer(new UserLinkColumnRenderer(UserEditForm::class))
- ->filter(new UserFilter()),
- GridViewColumn::for('ipAddress')
- ->label('wcf.user.ipAddress')
- ->sortable()
- ->renderer(new IpAddressColumnRenderer())
- ->filter(new IpAddressFilter()),
- GridViewColumn::for('userAgent')
- ->label('wcf.user.userAgent')
- ->sortable()
- ->valueEncoding(false)
- ->renderer(new TruncatedTextColumnRenderer(50))
- ->filter(new TextFilter()),
- GridViewColumn::for('time')
- ->label('wcf.acp.sessionLog.time')
- ->sortable()
- ->renderer(new TimeColumnRenderer())
- ->filter(new TimeFilter()),
- GridViewColumn::for('lastActivityTime')
- ->label('wcf.acp.sessionLog.lastActivityTime')
- ->sortable()
- ->renderer(new TimeColumnRenderer())
- ->filter(new TimeFilter()),
- GridViewColumn::for('accesses')
- ->label('wcf.acp.sessionLog.actions')
- ->sortable(true, 'accesses')
- ->renderer(new NumberColumnRenderer()),
- ]);
-
- $this->addRowLink(new GridViewRowLink(ACPSessionLogPage::class));
- $this->setSortField('lastActivityTime');
- $this->setSortOrder('DESC');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canViewLog');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- $list = new ACPSessionLogList();
- $list->sqlSelects .= "
- user_table.username,
- 0 AS active,
- (
- SELECT COUNT(*)
- FROM wcf1_acp_session_access_log
- WHERE sessionLogID = " . $list->getDatabaseTableAlias() . ".sessionLogID
- ) AS accesses";
- $list->sqlJoins = $list->sqlConditionJoins .= " LEFT JOIN wcf" . WCF_N . "_user user_table ON (user_table.userID = " . $list->getDatabaseTableAlias() . ".userID)";
-
- return $list;
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new ACPSessionLogGridViewInitialized($this);
- }
-}
use LogicException;
use wcf\action\GridViewFilterAction;
+use wcf\data\DatabaseObject;
use wcf\event\IPsr14Event;
use wcf\system\event\EventHandler;
-use wcf\system\gridView\action\IGridViewAction;
+use wcf\system\interaction\IInteraction;
+use wcf\system\interaction\IInteractionProvider;
+use wcf\system\interaction\InteractionContextMenuView;
use wcf\system\request\LinkHandler;
use wcf\system\WCF;
use wcf\util\StringUtil;
private array $columns = [];
/**
- * @var IGridViewAction[]
+ * @var IInteraction[]
*/
- private array $actions = [];
+ private array $quickInteractions = [];
private GridViewRowLink $rowLink;
private int $rowsPerPage = 20;
private int $pageNo = 1;
private array $activeFilters = [];
private string|int|null $objectIDFilter = null;
+ private ?IInteractionProvider $interactionProvider = null;
+ private InteractionContextMenuView $interactionContextMenuView;
/**
* Adds a new column to the grid view.
}
/**
- * Adds the given actions to the grid view.
- * @param IGridViewAction[] $columns
+ * Sets the interaction provider that is used to render the interaction context menu.
*/
- public function addActions(array $actions): void
+ public function setInteractionProvider(IInteractionProvider $provider): void
{
- foreach ($actions as $action) {
- $this->addAction($action);
- }
+ $this->interactionProvider = $provider;
}
/**
- * Adds the given action to the grid view.
+ * Returns the interaction provider of the grid view.
*/
- public function addAction(IGridViewAction $action): void
+ public function getInteractionProvider(): ?IInteractionProvider
{
- $this->actions[] = $action;
+ return $this->interactionProvider;
}
/**
- * Returns all actions of the grid view.
- * @return IGridViewAction[]
+ * Returns true, if this grid view has interactions.
*/
- public function getActions(): array
+ public function hasInteractions(): bool
{
- return $this->actions;
+ return $this->interactionProvider !== null || $this->quickInteractions !== [];
}
/**
- * Returns true, if this grid view has actions.
+ * Adds a quick interaction.
*/
- public function hasActions(): bool
+ public function addQuickInteraction(IInteraction $interaction): void
{
- return $this->actions !== [];
+ $this->quickInteractions[] = $interaction;
}
/**
- * Returns true, if this grid view has actions that should be displayed in the dropdown.
+ * Returns the quick interactions.
+ * @return IInteraction[]
*/
- public function hasDropdownActions(): bool
+ public function getQuickInteractions(): array
{
- return $this->getDropdownActions() !== [];
- }
-
- /**
- * Returns the actions that should be displayed in the dropdown.
- * @return IGridViewAction[]
- */
- public function getDropdownActions(): array
- {
- return \array_filter($this->getActions(), fn($action) => !$action->isQuickAction());
- }
-
- /**
- * Returns the quick actions.
- * @return IGridViewAction[]
- */
- public function getQuickActions(): array
- {
- return \array_filter($this->getActions(), fn($action) => $action->isQuickAction());
+ return $this->quickInteractions;
}
/**
}
/**
- * Renders the given action.
+ * Returns the view of the interaction context menu.
*/
- public function renderAction(IGridViewAction $action, mixed $row): string
+ public function getInteractionContextMenuView(): InteractionContextMenuView
{
- return $action->render($row);
+ if ($this->interactionProvider === null) {
+ throw new \BadMethodCallException("Missing interaction provider.");
+ }
+
+ if (!isset($this->interactionContextMenuView)) {
+ $this->interactionContextMenuView = new InteractionContextMenuView($this->interactionProvider);
+ }
+
+ return $this->interactionContextMenuView;
}
/**
- * Renders the initialization code for the actions of the grid view.
+ * Renders the initialization code for the interactions of the grid view.
*/
- public function renderActionInitialization(): string
+ public function renderInteractionInitialization(): string
{
- return implode(
- "\n",
- \array_map(
- fn($action) => $action->renderInitialization($this),
- $this->getActions()
- )
- );
+ $code = '';
+ if ($this->interactionProvider !== null) {
+ $code = $this->getInteractionContextMenuView()->renderInitialization($this->getID() . '_table');
+ }
+
+ if ($this->quickInteractions !== []) {
+ $code .= "\n";
+ $code .= \implode("\n", \array_map(
+ fn($interaction) => $interaction->renderInitialization($this->getID() . '_table'),
+ $this->getQuickInteractions()
+ ));
+ }
+
+ return $code;
+ }
+
+ /**
+ * Renders the interactions for the given row.
+ */
+ public function renderInteractionContextMenuButton(mixed $row): string
+ {
+ if ($this->interactionProvider === null) {
+ return '';
+ }
+
+ \assert($row instanceof DatabaseObject);
+
+ return $this->getInteractionContextMenuView()->renderButton($row);
+ }
+
+ /**
+ * Renders the interactions for the given row.
+ */
+ public function renderQuickInteractions(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+
+ return \implode("\n", \array_map(
+ static fn($interaction) => $interaction->render($row),
+ $this->getQuickInteractions()
+ ));
}
/**
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use wcf\data\cronjob\Cronjob;
-use wcf\data\cronjob\I18nCronjobList;
-use wcf\data\cronjob\log\CronjobLog;
-use wcf\data\cronjob\log\CronjobLogList;
-use wcf\data\DatabaseObjectList;
-use wcf\event\gridView\CronjobLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\filter\SelectFilter;
-use wcf\system\gridView\filter\TimeFilter;
-use wcf\system\gridView\renderer\DefaultColumnRenderer;
-use wcf\system\gridView\renderer\NumberColumnRenderer;
-use wcf\system\gridView\renderer\TimeColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Grid view for the cronjob log.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class CronjobLogGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $availableCronjobs = $this->getAvailableCronjobs();
-
- $this->addColumns([
- GridViewColumn::for('cronjobLogID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('cronjobID')
- ->label('wcf.acp.cronjob')
- ->sortable()
- ->titleColumn()
- ->filter(new SelectFilter($availableCronjobs))
- ->renderer([
- new class($availableCronjobs) extends DefaultColumnRenderer {
- public function __construct(private readonly array $availableCronjobs) {}
-
- public function render(mixed $value, mixed $context = null): string
- {
- return $this->availableCronjobs[$value];
- }
- },
- ]),
- GridViewColumn::for('execTime')
- ->label('wcf.acp.cronjob.log.execTime')
- ->sortable()
- ->filter(new TimeFilter())
- ->renderer(new TimeColumnRenderer()),
- GridViewColumn::for('success')
- ->label('wcf.acp.cronjob.log.status')
- ->sortable()
- ->filter(new SelectFilter([
- 1 => 'wcf.acp.cronjob.log.success',
- 0 => 'wcf.acp.cronjob.log.error',
- ]))
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof CronjobLog);
-
- if ($context->success) {
- return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
- }
- if ($context->error) {
- $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
- $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
- $id = 'cronjobLogError' . $context->cronjobLogID;
- $error = StringUtil::encodeHTML($context->error);
- $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
-
- return <<<HTML
- <button type="button" id="{$buttonId}" class="badge red">
- {$label}
- </button>
- <template id="{$id}">{$error}</template>
- <script data-relocate="true">
- require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
- document.getElementById('{$buttonId}').addEventListener('click', () => {
- const dialog = dialogFactory().fromId('{$id}').withoutControls();
- dialog.show('{$dialogTitle}');
- });
- });
- </script>
- HTML;
- }
-
- return '';
- }
- },
- ]),
- ]);
-
- $this->setSortField('execTime');
- $this->setSortOrder('DESC');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canManageCronjob');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- return new CronjobLogList();
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new CronjobLogGridViewInitialized($this);
- }
-
- private function getAvailableCronjobs(): array
- {
- $list = new I18nCronjobList();
- $list->sqlOrderBy = 'descriptionI18n';
- $list->readObjects();
-
- return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use wcf\event\gridView\ExceptionLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\Regex;
-use wcf\system\gridView\filter\SelectFilter;
-use wcf\system\gridView\filter\TextFilter;
-use wcf\system\gridView\renderer\TimeColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\DirectoryUtil;
-use wcf\util\ExceptionLogUtil;
-
-/**
- * Grid view for the exception log.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ExceptionLogGridView extends DataSourceGridView
-{
- private array $availableLogFiles;
-
- public function __construct(bool $applyDefaultFilter = false)
- {
- $this->addColumns([
- GridViewColumn::for('message')
- ->label('wcf.acp.exceptionLog.exception.message')
- ->sortable()
- ->titleColumn(),
- GridViewColumn::for('exceptionID')
- ->label('wcf.acp.exceptionLog.search.exceptionID')
- ->filter(new TextFilter())
- ->sortable(),
- GridViewColumn::for('date')
- ->label('wcf.acp.exceptionLog.exception.date')
- ->sortable()
- ->renderer(new TimeColumnRenderer()),
- GridViewColumn::for('logFile')
- ->label('wcf.acp.exceptionLog.search.logFile')
- ->filter(new SelectFilter($this->getAvailableLogFiles()))
- ->hidden(true),
- ]);
-
- $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
- $this->setSortField('date');
- $this->setSortOrder('DESC');
-
- if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
- $this->setActiveFilters([
- 'logFile' => $this->getDefaultLogFile(),
- ]);
- }
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canViewLog');
- }
-
- #[\Override]
- public function getObjectID(mixed $row): mixed
- {
- return $row['exceptionID'];
- }
-
- #[\Override]
- protected function loadDataSource(): array
- {
- if (!empty($this->getActiveFilters()['exceptionID'])) {
- $exceptionID = $this->getActiveFilters()['exceptionID'];
- $contents = $logFile = '';
- foreach ($this->getAvailableLogFiles() as $logFile) {
- $contents = \file_get_contents(WCF_DIR . $logFile);
-
- if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
- break;
- }
-
- unset($contents);
- }
-
- if ($contents === '') {
- return [];
- }
-
- $exceptions = ExceptionLogUtil::splitLog($contents);
- $parsedExceptions = [];
-
- foreach ($exceptions as $key => $val) {
- if ($key !== $exceptionID) {
- continue;
- }
-
- $parsed = ExceptionLogUtil::parseException($val);
-
- $parsedExceptions[$key] = [
- 'exceptionID' => $key,
- 'message' => $parsed['message'],
- 'date' => $parsed['date'],
- 'logFile' => $logFile,
- ];
- }
-
- return $parsedExceptions;
- } elseif (!empty($this->getActiveFilters()['logFile'])) {
- $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['logFile']);
- $exceptions = ExceptionLogUtil::splitLog($contents);
- $parsedExceptions = [];
-
- foreach ($exceptions as $key => $val) {
- $parsed = ExceptionLogUtil::parseException($val);
-
- $parsedExceptions[$key] = [
- 'exceptionID' => $key,
- 'message' => $parsed['message'],
- 'date' => $parsed['date'],
- 'logFile' => $this->getActiveFilters()['logFile'],
- ];
- }
-
- return $parsedExceptions;
- }
-
- return [];
- }
-
- #[\Override]
- protected function applyFilters(): void
- {
- // Overwrite the default filtering, as this is already applied when the data is loaded.
- }
-
- private function getAvailableLogFiles(): array
- {
- if (!isset($this->availableLogFiles)) {
- $this->availableLogFiles = [];
- $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
- $logFiles = DirectoryUtil::getInstance(WCF_DIR . 'log/', false)->getFiles(\SORT_DESC, $fileNameRegex);
- foreach ($logFiles as $logFile) {
- $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
- }
- }
-
- return $this->availableLogFiles;
- }
-
- private function getDefaultLogFile(): ?string
- {
- return \array_key_first($this->getAvailableLogFiles());
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new ExceptionLogGridViewInitialized($this);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use LogicException;
-use wcf\data\DatabaseObject;
-use wcf\data\DatabaseObjectDecorator;
-use wcf\data\DatabaseObjectList;
-use wcf\data\modification\log\IViewableModificationLog;
-use wcf\data\modification\log\ModificationLog;
-use wcf\data\modification\log\ModificationLogList;
-use wcf\data\object\type\ObjectTypeCache;
-use wcf\data\package\Package;
-use wcf\event\gridView\ModificationLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\SelectFormField;
-use wcf\system\gridView\filter\IGridViewFilter;
-use wcf\system\gridView\filter\TextFilter;
-use wcf\system\gridView\filter\TimeFilter;
-use wcf\system\gridView\renderer\DefaultColumnRenderer;
-use wcf\system\gridView\renderer\ILinkColumnRenderer;
-use wcf\system\gridView\renderer\ObjectIdColumnRenderer;
-use wcf\system\gridView\renderer\TimeColumnRenderer;
-use wcf\system\gridView\renderer\UserLinkColumnRenderer;
-use wcf\system\log\modification\IExtendedModificationLogHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Grid view for the list of all modification log items.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class ModificationLogGridView extends DatabaseObjectListGridView
-{
- /**
- * @var IViewableModificationLog[]
- */
- private array $logItems;
-
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('logID')
- ->label('wcf.global.objectID')
- ->renderer(new ObjectIdColumnRenderer())
- ->sortable(),
- GridViewColumn::for('userID')
- ->label('wcf.user.username')
- ->sortable(true, 'modification_log.username')
- ->renderer(new UserLinkColumnRenderer())
- ->filter(new TextFilter('modification_log.username')),
- GridViewColumn::for('action')
- ->label('wcf.acp.modificationLog.action')
- ->titleColumn()
- ->renderer([
- new class extends DefaultColumnRenderer {
- #[\Override]
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof DatabaseObjectDecorator);
- $log = $context->getDecoratedObject();
- \assert($log instanceof ModificationLog);
- $objectType = ObjectTypeCache::getInstance()->getObjectType($log->objectTypeID);
- if (!$objectType) {
- return '';
- }
-
- return WCF::getLanguage()->get(
- 'wcf.acp.modificationLog.' . $objectType->objectType . '.' . $log->action
- );
- }
- },
- ])
- ->filter(
- new class($this->getAvailableActions()) implements IGridViewFilter {
- public function __construct(private readonly array $options) {}
-
- #[\Override]
- public function getFormField(string $id, string $label): AbstractFormField
- {
- return SelectFormField::create($id)
- ->label($label)
- ->options($this->options, false, false);
- }
-
- #[\Override]
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
- {
- if (\is_numeric($value)) {
- $list->getConditionBuilder()->add(
- "objectTypeID IN (SELECT objectTypeID FROM wcf1_object_type WHERE packageID = ?)",
- [$value]
- );
- } else if (\preg_match('~^(?P<objectType>.+)\-(?P<action>[^\-]+)$~', $value, $matches)) {
- $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
- 'com.woltlab.wcf.modifiableContent',
- $matches['objectType']
- );
- if (!$objectType) {
- return;
- }
-
- $list->getConditionBuilder()->add(
- "objectTypeID = ? AND action = ?",
- [$objectType->objectTypeID, $matches['action']]
- );
- }
- }
-
- #[\Override]
- public function matches(string $filterValue, string $rowValue): bool
- {
- throw new LogicException('unreachable');
- }
-
- #[\Override]
- public function renderValue(string $value): string
- {
- if (\is_numeric($value)) {
- return WCF::getLanguage()->get($this->options[$value]);
- }
-
- return \substr(WCF::getLanguage()->get($this->options[$value]), 24);
- }
- }
- ),
- GridViewColumn::for('affectedObject')
- ->label('wcf.acp.modificationLog.affectedObject')
- ->renderer([
- new class extends DefaultColumnRenderer implements ILinkColumnRenderer {
- #[\Override]
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof IViewableModificationLog);
- if ($context->getAffectedObject() === null) {
- return WCF::getLanguage()->get('wcf.acp.modificationLog.affectedObject.unknown');
- }
-
- return \sprintf(
- '<a href="%s">%s</a>',
- StringUtil::encodeHTML($context->getAffectedObject()->getLink()),
- StringUtil::encodeHTML($context->getAffectedObject()->getTitle())
- );
- }
- },
- ]),
- GridViewColumn::for('time')
- ->label('wcf.global.date')
- ->sortable()
- ->renderer(new TimeColumnRenderer())
- ->filter(new TimeFilter()),
- ]);
-
- $this->setSortField('time');
- $this->setSortOrder('DESC');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canViewLog');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- return new ModificationLogList();
- }
-
- #[\Override]
- public function getRows(): array
- {
- if (!isset($this->logItems)) {
- $this->logItems = [];
- $this->getObjectList()->readObjects();
-
- $itemsPerType = [];
- foreach ($this->getObjectList() as $modificationLog) {
- if (!isset($itemsPerType[$modificationLog->objectTypeID])) {
- $itemsPerType[$modificationLog->objectTypeID] = [];
- }
-
- $itemsPerType[$modificationLog->objectTypeID][] = $modificationLog;
- }
-
- if (!empty($itemsPerType)) {
- foreach ($itemsPerType as $objectTypeID => $items) {
- $objectType = ObjectTypeCache::getInstance()->getObjectType($objectTypeID);
- if (!$objectType) {
- continue;
- }
- $processor = $objectType->getProcessor();
- if (!$processor) {
- continue;
- }
- \assert($processor instanceof IExtendedModificationLogHandler);
-
- $this->logItems = \array_merge(
- $this->logItems,
- $processor->processItems($items)
- );
- }
- }
-
- DatabaseObject::sort($this->logItems, $this->getSortField(), $this->getSortOrder());
- }
-
- return $this->logItems;
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new ModificationLogGridViewInitialized($this);
- }
-
- private function getAvailableActions(): array
- {
- $packages = $actions = $availableActions = [];
-
- foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.modifiableContent') as $objectType) {
- $processor = $objectType->getProcessor();
- if ($processor === null) {
- continue;
- }
- \assert($processor instanceof IExtendedModificationLogHandler);
-
- if (!$processor->includeInLogList()) {
- continue;
- }
-
- if (!isset($packages[$objectType->packageID])) {
- $actions[$objectType->packageID] = [];
- $packages[$objectType->packageID] = $objectType->getPackage();
- }
-
- foreach ($processor->getAvailableActions() as $action) {
- $actions[$objectType->packageID]["{$objectType->objectType}-{$action}"]
- = WCF::getLanguage()->get("wcf.acp.modificationLog.{$objectType->objectType}.{$action}");
- }
- }
-
- foreach ($actions as &$actionsPerPackage) {
- \asort($actionsPerPackage, \SORT_NATURAL);
- }
- \uasort($packages, static function (Package $a, Package $b) {
- return \strnatcasecmp($a->package, $b->package);
- });
-
- foreach ($packages as $package) {
- $availableActions[$package->packageID]
- = WCF::getLanguage()->getDynamicVariable(
- 'wcf.acp.modificationLog.action.allPackageActions',
- ['package' => $package]
- );
-
- foreach ($actions[$package->packageID] as $actionName => $actionLabel) {
- $availableActions[$actionName] = \str_repeat(' ', 4) . $actionLabel;
- }
- }
-
- return $availableActions;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use wcf\acp\form\UserOptionEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\option\UserOption;
-use wcf\data\user\option\UserOptionList;
-use wcf\event\gridView\UserOptionGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\action\DeleteAction;
-use wcf\system\gridView\action\EditAction;
-use wcf\system\gridView\action\ToggleAction;
-use wcf\system\gridView\renderer\DefaultColumnRenderer;
-use wcf\system\gridView\renderer\NumberColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Grid view for the list of user options.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class UserOptionGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('optionID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('optionName')
- ->label('wcf.global.name')
- ->sortable()
- ->titleColumn()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserOption);
-
- return StringUtil::encodeHTML($context->getTitle());
- }
- }
- ]),
- GridViewColumn::for('categoryName')
- ->label('wcf.global.category')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserOption);
-
- return StringUtil::encodeHTML(
- WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
- );
- }
- }
- ]),
- GridViewColumn::for('optionType')
- ->label('wcf.acp.user.option.optionType')
- ->sortable(),
- GridViewColumn::for('showOrder')
- ->label('wcf.global.showOrder')
- ->sortable()
- ->renderer(new NumberColumnRenderer()),
- ]);
-
- $this->addActions([
- new ToggleAction('core/users/options/%s/enable', 'core/users/options/%s/disable'),
- new EditAction(UserOptionEditForm::class),
- new DeleteAction('core/users/options/%s', fn(UserOption $row) => $row->canDelete()),
- ]);
- $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
- $this->setSortField('showOrder');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.user.canManageUserOption');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- $list = new UserOptionList();
- $list->getConditionBuilder()->add(
- "option_table.categoryName IN (
- SELECT categoryName
- FROM wcf" . WCF_N . "_user_option_category
- WHERE parentCategoryName = ?
- )",
- ['profile']
- );
-
- return $list;
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new UserOptionGridViewInitialized($this);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView;
-
-use wcf\acp\form\UserRankEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\group\UserGroup;
-use wcf\data\user\rank\I18nUserRankList;
-use wcf\data\user\rank\UserRank;
-use wcf\event\gridView\UserRankGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\gridView\action\DeleteAction;
-use wcf\system\gridView\action\EditAction;
-use wcf\system\gridView\filter\I18nTextFilter;
-use wcf\system\gridView\filter\SelectFilter;
-use wcf\system\gridView\renderer\DefaultColumnRenderer;
-use wcf\system\gridView\renderer\NumberColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Grid view for the list of user ranks.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-final class UserRankGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('rankID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('rankTitle')
- ->label('wcf.acp.user.rank.title')
- ->sortable(true, 'rankTitleI18n')
- ->titleColumn()
- ->filter(new I18nTextFilter())
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserRank);
-
- return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
- . StringUtil::encodeHTML($context->getTitle())
- . '<span>';
- }
- }
- ]),
- GridViewColumn::for('rankImage')
- ->label('wcf.acp.user.rank.image')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserRank);
-
- return $context->rankImage ? $context->getImage() : '';
- }
- },
- ]),
- GridViewColumn::for('groupID')
- ->label('wcf.user.group')
- ->sortable()
- ->filter(new SelectFilter($this->getAvailableUserGroups()))
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
- }
- },
- ]),
- GridViewColumn::for('requiredGender')
- ->label('wcf.user.option.gender')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- if (!$value) {
- return '';
- }
-
- return WCF::getLanguage()->get(match ($value) {
- 1 => 'wcf.user.gender.male',
- 2 => 'wcf.user.gender.female',
- default => 'wcf.user.gender.other'
- });
- }
- },
- ]),
- GridViewColumn::for('requiredPoints')
- ->label('wcf.acp.user.rank.requiredPoints')
- ->sortable()
- ->renderer(new NumberColumnRenderer()),
- ]);
-
- $this->addActions([
- new EditAction(UserRankEditForm::class),
- new DeleteAction('core/users/ranks/%s'),
- ]);
- $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
- $this->setSortField('rankTitle');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- return new I18nUserRankList();
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new UserRankGridViewInitialized($this);
- }
-
- private function getAvailableUserGroups(): array
- {
- $groups = [];
- foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
- $groups[$group->groupID] = $group->getName();
- }
-
- return $groups;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\system\gridView\AbstractGridView;
-
-/**
- * Provides an abstract implementation of a grid view action.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-abstract class AbstractAction implements IGridViewAction
-{
- public function __construct(
- protected readonly ?Closure $isAvailableCallback = null
- ) {}
-
- #[\Override]
- public function isAvailable(mixed $row): bool
- {
- if ($this->isAvailableCallback === null) {
- return true;
- }
-
- return ($this->isAvailableCallback)($row);
- }
-
- #[\Override]
- public function isQuickAction(): bool
- {
- return false;
- }
-
- #[\Override]
- public function renderInitialization(AbstractGridView $gridView): ?string
- {
- return null;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-enum ActionConfirmationType
-{
- case None;
- case SoftDelete;
- case SoftDeleteWithReason;
- case Restore;
- case Delete;
- case Custom;
-
- public function toString(): string
- {
- return match ($this) {
- self::None => 'None',
- self::SoftDelete => 'SoftDelete',
- self::SoftDeleteWithReason => 'SoftDeleteWithReason',
- self::Restore => 'Restore',
- self::Delete => 'Delete',
- self::Custom => 'Custom',
- };
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\data\ITitledObject;
-use wcf\system\gridView\AbstractGridView;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Represents a delete action.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-class DeleteAction extends AbstractAction
-{
- public function __construct(
- private readonly string $endpoint,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- $label = WCF::getLanguage()->get('wcf.global.button.delete');
- $endpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->endpoint, $row->getObjectID())
- );
- 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\gridView\action;
-
-use Closure;
-
-/**
- * Represents an edit action.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-class EditAction extends LinkAction
-{
- public function __construct(
- string $controllerClass,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($controllerClass, 'wcf.global.button.edit', $isAvailableCallback);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use wcf\system\gridView\AbstractGridView;
-
-/**
- * Represents an action of a grid view.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-interface IGridViewAction
-{
- /**
- * Renders the action.
- */
- public function render(mixed $row): string;
-
- /**
- * Renders the initialization code for this action.
- */
- public function renderInitialization(AbstractGridView $gridView): ?string;
-
- /**
- * Returns true if this is a quick action.
- */
- public function isQuickAction(): bool;
-
- /**
- * Returns true if this action is available for the given row.
- */
- public function isAvailable(mixed $row): bool;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\data\DatabaseObject;
-use wcf\data\ITitledObject;
-use wcf\system\gridView\AbstractGridView;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Represents an action that executes a dbo action.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
- */
-class LegacyDboAction extends AbstractAction
-{
- public function __construct(
- protected readonly string $className,
- protected readonly string $actionName,
- protected readonly string|Closure $languageItem,
- protected readonly ActionConfirmationType $confirmationType = ActionConfirmationType::None,
- protected readonly string|Closure $confirmationMessage = '',
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- if (\is_string($this->languageItem)) {
- $label = WCF::getLanguage()->get($this->languageItem);
- } else {
- $label = ($this->languageItem)($row);
- }
-
- if (\is_string($this->confirmationMessage)) {
- $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage);
- } else {
- $confirmationMessage = ($this->confirmationMessage)($row);
- }
-
- if ($row instanceof ITitledObject) {
- $objectName = StringUtil::encodeHTML($row->getTitle());
- } else {
- $objectName = '';
- }
-
- $className = StringUtil::encodeHTML($this->className);
- $actionName = StringUtil::encodeHTML($this->actionName);
-
- return <<<HTML
- <button
- type="button"
- data-action="legacy-dbo-action"
- data-object-name="{$objectName}"
- data-class-name="{$className}"
- data-action-name="{$actionName}"
- data-confirmation-type="{$this->confirmationType->toString()}"
- data-confirmation-message="{$confirmationMessage}"
- >
- {$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/LegacyDboAction'], ({ setup }) => {
- setup(document.getElementById('{$id}_table'));
- });
- </script>
- HTML;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\data\DatabaseObject;
-use wcf\system\gridView\AbstractGridView;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Represents an action that links to a given controller.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-class LinkAction extends AbstractAction
-{
- public function __construct(
- protected readonly string $controllerClass,
- protected readonly string|Closure $languageItem,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
- $href = LinkHandler::getInstance()->getControllerLink(
- $this->controllerClass,
- ['object' => $row]
- );
-
- if (\is_string($this->languageItem)) {
- $title = WCF::getLanguage()->get($this->languageItem);
- } else {
- $title = ($this->languageItem)($row);
- }
-
- return \sprintf('<a href="%s">%s</a>', StringUtil::encodeHTML($href), $title);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\data\ITitledObject;
-use wcf\system\gridView\AbstractGridView;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Represents an action that call a rpc endpoint.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-class RpcAction extends AbstractAction
-{
- public function __construct(
- protected readonly string $endpoint,
- protected readonly string|Closure $languageItem,
- protected readonly ActionConfirmationType $confirmationType = ActionConfirmationType::None,
- protected readonly string|Closure $confirmationMessage = '',
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- if (\is_string($this->languageItem)) {
- $label = WCF::getLanguage()->get($this->languageItem);
- } else {
- $label = ($this->languageItem)($row);
- }
-
- if (\is_string($this->confirmationMessage)) {
- $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage);
- } else {
- $confirmationMessage = ($this->confirmationMessage)($row);
- }
-
- $endpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->endpoint, $row->getObjectID())
- );
-
- if ($row instanceof ITitledObject) {
- $objectName = StringUtil::encodeHTML($row->getTitle());
- } else {
- $objectName = '';
- }
-
- return <<<HTML
- <button
- type="button"
- data-action="rpc"
- data-object-name="{$objectName}"
- data-endpoint="{$endpoint}"
- data-confirmation-type="{$this->confirmationType->toString()}"
- data-confirmation-message="{$confirmationMessage}"
- >
- {$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/Rpc'], ({ setup }) => {
- setup(document.getElementById('{$id}_table'));
- });
- </script>
- HTML;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\gridView\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\system\gridView\AbstractGridView;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-/**
- * Represents a toggle action.
- *
- * @author Marcel Werk
- * @copyright 2001-2024 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @since 6.2
- */
-class ToggleAction extends AbstractAction
-{
- public function __construct(
- private readonly string $enableEndpoint,
- private readonly string $disableEndpoint,
- private readonly string $propertyName = 'isDisabled',
- private readonly bool $propertyIsDisabledState = true,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- $enableEndpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->enableEndpoint, $row->getObjectID())
- );
- $disableEndpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->disableEndpoint, $row->getObjectID())
- );
-
- $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
- $checked = (!$row->{$this->propertyName} && $this->propertyIsDisabledState)
- || ($row->{$this->propertyName} && !$this->propertyIsDisabledState) ? 'checked' : '';
-
- return <<<HTML
- <woltlab-core-toggle-button aria-label="{$ariaLabel}" data-enable-endpoint="{$enableEndpoint}" data-disable-endpoint="{$disableEndpoint}" {$checked}></woltlab-core-toggle-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/Toggle'], ({ setup }) => {
- setup('{$id}_table');
- });
- </script>
- HTML;
- }
-
- #[\Override]
- public function isQuickAction(): bool
- {
- return true;
- }
-}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use wcf\acp\form\UserEditForm;
+use wcf\acp\page\ACPSessionLogPage;
+use wcf\data\acp\session\log\ACPSessionLogList;
+use wcf\data\DatabaseObjectList;
+use wcf\event\gridView\admin\ACPSessionLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\DatabaseObjectListGridView;
+use wcf\system\gridView\filter\IpAddressFilter;
+use wcf\system\gridView\filter\TextFilter;
+use wcf\system\gridView\filter\TimeFilter;
+use wcf\system\gridView\filter\UserFilter;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\GridViewRowLink;
+use wcf\system\gridView\renderer\IpAddressColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\TruncatedTextColumnRenderer;
+use wcf\system\gridView\renderer\UserLinkColumnRenderer;
+use wcf\system\WCF;
+
+/**
+ * Grid view for the list of logged admin session.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ACPSessionLogGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('sessionLogID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('userID')
+ ->label('wcf.user.username')
+ ->sortable(true, 'user_table.username')
+ ->titleColumn()
+ ->renderer(new UserLinkColumnRenderer(UserEditForm::class))
+ ->filter(new UserFilter()),
+ GridViewColumn::for('ipAddress')
+ ->label('wcf.user.ipAddress')
+ ->sortable()
+ ->renderer(new IpAddressColumnRenderer())
+ ->filter(new IpAddressFilter()),
+ GridViewColumn::for('userAgent')
+ ->label('wcf.user.userAgent')
+ ->sortable()
+ ->valueEncoding(false)
+ ->renderer(new TruncatedTextColumnRenderer(50))
+ ->filter(new TextFilter()),
+ GridViewColumn::for('time')
+ ->label('wcf.acp.sessionLog.time')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer())
+ ->filter(new TimeFilter()),
+ GridViewColumn::for('lastActivityTime')
+ ->label('wcf.acp.sessionLog.lastActivityTime')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer())
+ ->filter(new TimeFilter()),
+ GridViewColumn::for('accesses')
+ ->label('wcf.acp.sessionLog.actions')
+ ->sortable(true, 'accesses')
+ ->renderer(new NumberColumnRenderer()),
+ ]);
+
+ $this->addRowLink(new GridViewRowLink(ACPSessionLogPage::class));
+ $this->setSortField('lastActivityTime');
+ $this->setSortOrder('DESC');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canViewLog');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ $list = new ACPSessionLogList();
+ $list->sqlSelects .= "
+ user_table.username,
+ 0 AS active,
+ (
+ SELECT COUNT(*)
+ FROM wcf1_acp_session_access_log
+ WHERE sessionLogID = " . $list->getDatabaseTableAlias() . ".sessionLogID
+ ) AS accesses";
+ $list->sqlJoins = $list->sqlConditionJoins .= " LEFT JOIN wcf" . WCF_N . "_user user_table ON (user_table.userID = " . $list->getDatabaseTableAlias() . ".userID)";
+
+ return $list;
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new ACPSessionLogGridViewInitialized($this);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use wcf\data\cronjob\Cronjob;
+use wcf\data\cronjob\I18nCronjobList;
+use wcf\data\cronjob\log\CronjobLog;
+use wcf\data\cronjob\log\CronjobLogList;
+use wcf\data\DatabaseObjectList;
+use wcf\event\gridView\admin\CronjobLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\DatabaseObjectListGridView;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TimeFilter;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Grid view for the cronjob log.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class CronjobLogGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $availableCronjobs = $this->getAvailableCronjobs();
+
+ $this->addColumns([
+ GridViewColumn::for('cronjobLogID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('cronjobID')
+ ->label('wcf.acp.cronjob')
+ ->sortable()
+ ->titleColumn()
+ ->filter(new SelectFilter($availableCronjobs))
+ ->renderer([
+ new class($availableCronjobs) extends DefaultColumnRenderer {
+ public function __construct(private readonly array $availableCronjobs) {}
+
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return $this->availableCronjobs[$value];
+ }
+ },
+ ]),
+ GridViewColumn::for('execTime')
+ ->label('wcf.acp.cronjob.log.execTime')
+ ->sortable()
+ ->filter(new TimeFilter())
+ ->renderer(new TimeColumnRenderer()),
+ GridViewColumn::for('success')
+ ->label('wcf.acp.cronjob.log.status')
+ ->sortable()
+ ->filter(new SelectFilter([
+ 1 => 'wcf.acp.cronjob.log.success',
+ 0 => 'wcf.acp.cronjob.log.error',
+ ]))
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof CronjobLog);
+
+ if ($context->success) {
+ return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
+ }
+ if ($context->error) {
+ $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
+ $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
+ $id = 'cronjobLogError' . $context->cronjobLogID;
+ $error = StringUtil::encodeHTML($context->error);
+ $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
+
+ return <<<HTML
+ <button type="button" id="{$buttonId}" class="badge red">
+ {$label}
+ </button>
+ <template id="{$id}"><pre>{$error}</pre></template>
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
+ document.getElementById('{$buttonId}').addEventListener('click', () => {
+ const dialog = dialogFactory().fromId('{$id}').withoutControls();
+ dialog.show('{$dialogTitle}');
+ });
+ });
+ </script>
+ HTML;
+ }
+
+ return '';
+ }
+ },
+ ]),
+ ]);
+
+ $this->setSortField('execTime');
+ $this->setSortOrder('DESC');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canManageCronjob');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new CronjobLogList();
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new CronjobLogGridViewInitialized($this);
+ }
+
+ private function getAvailableCronjobs(): array
+ {
+ $list = new I18nCronjobList();
+ $list->sqlOrderBy = 'descriptionI18n';
+ $list->readObjects();
+
+ return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use wcf\event\gridView\admin\ExceptionLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\DataSourceGridView;
+use wcf\system\Regex;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TextFilter;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\GridViewRowLink;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
+use wcf\util\ExceptionLogUtil;
+
+/**
+ * Grid view for the exception log.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ExceptionLogGridView extends DataSourceGridView
+{
+ private array $availableLogFiles;
+
+ public function __construct(bool $applyDefaultFilter = false)
+ {
+ $this->addColumns([
+ GridViewColumn::for('message')
+ ->label('wcf.acp.exceptionLog.exception.message')
+ ->sortable()
+ ->titleColumn(),
+ GridViewColumn::for('exceptionID')
+ ->label('wcf.acp.exceptionLog.search.exceptionID')
+ ->filter(new TextFilter())
+ ->sortable(),
+ GridViewColumn::for('date')
+ ->label('wcf.acp.exceptionLog.exception.date')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer()),
+ GridViewColumn::for('logFile')
+ ->label('wcf.acp.exceptionLog.search.logFile')
+ ->filter(new SelectFilter($this->getAvailableLogFiles()))
+ ->hidden(true),
+ ]);
+
+ $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
+ $this->setSortField('date');
+ $this->setSortOrder('DESC');
+
+ if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
+ $this->setActiveFilters([
+ 'logFile' => $this->getDefaultLogFile(),
+ ]);
+ }
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canViewLog');
+ }
+
+ #[\Override]
+ public function getObjectID(mixed $row): mixed
+ {
+ return $row['exceptionID'];
+ }
+
+ #[\Override]
+ protected function loadDataSource(): array
+ {
+ if (!empty($this->getActiveFilters()['exceptionID'])) {
+ $exceptionID = $this->getActiveFilters()['exceptionID'];
+ $contents = $logFile = '';
+ foreach ($this->getAvailableLogFiles() as $logFile) {
+ $contents = \file_get_contents(WCF_DIR . $logFile);
+
+ if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
+ break;
+ }
+
+ unset($contents);
+ }
+
+ if ($contents === '') {
+ return [];
+ }
+
+ $exceptions = ExceptionLogUtil::splitLog($contents);
+ $parsedExceptions = [];
+
+ foreach ($exceptions as $key => $val) {
+ if ($key !== $exceptionID) {
+ continue;
+ }
+
+ $parsed = ExceptionLogUtil::parseException($val);
+
+ $parsedExceptions[$key] = [
+ 'exceptionID' => $key,
+ 'message' => $parsed['message'],
+ 'date' => $parsed['date'],
+ 'logFile' => $logFile,
+ ];
+ }
+
+ return $parsedExceptions;
+ } elseif (!empty($this->getActiveFilters()['logFile'])) {
+ $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['logFile']);
+ $exceptions = ExceptionLogUtil::splitLog($contents);
+ $parsedExceptions = [];
+
+ foreach ($exceptions as $key => $val) {
+ $parsed = ExceptionLogUtil::parseException($val);
+
+ $parsedExceptions[$key] = [
+ 'exceptionID' => $key,
+ 'message' => $parsed['message'],
+ 'date' => $parsed['date'],
+ 'logFile' => $this->getActiveFilters()['logFile'],
+ ];
+ }
+
+ return $parsedExceptions;
+ }
+
+ return [];
+ }
+
+ #[\Override]
+ protected function applyFilters(): void
+ {
+ // Overwrite the default filtering, as this is already applied when the data is loaded.
+ }
+
+ private function getAvailableLogFiles(): array
+ {
+ if (!isset($this->availableLogFiles)) {
+ $this->availableLogFiles = [];
+ $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
+ $logFiles = DirectoryUtil::getInstance(WCF_DIR . 'log/', false)->getFiles(\SORT_DESC, $fileNameRegex);
+ foreach ($logFiles as $logFile) {
+ $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
+ }
+ }
+
+ return $this->availableLogFiles;
+ }
+
+ private function getDefaultLogFile(): ?string
+ {
+ return \array_key_first($this->getAvailableLogFiles());
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new ExceptionLogGridViewInitialized($this);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use LogicException;
+use wcf\data\DatabaseObject;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\data\DatabaseObjectList;
+use wcf\data\modification\log\IViewableModificationLog;
+use wcf\data\modification\log\ModificationLog;
+use wcf\data\modification\log\ModificationLogList;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\package\Package;
+use wcf\event\gridView\admin\ModificationLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\SelectFormField;
+use wcf\system\gridView\DatabaseObjectListGridView;
+use wcf\system\gridView\filter\IGridViewFilter;
+use wcf\system\gridView\filter\TextFilter;
+use wcf\system\gridView\filter\TimeFilter;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\ILinkColumnRenderer;
+use wcf\system\gridView\renderer\ObjectIdColumnRenderer;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\UserLinkColumnRenderer;
+use wcf\system\log\modification\IExtendedModificationLogHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Grid view for the list of all modification log items.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class ModificationLogGridView extends DatabaseObjectListGridView
+{
+ /**
+ * @var IViewableModificationLog[]
+ */
+ private array $logItems;
+
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('logID')
+ ->label('wcf.global.objectID')
+ ->renderer(new ObjectIdColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('userID')
+ ->label('wcf.user.username')
+ ->sortable(true, 'modification_log.username')
+ ->renderer(new UserLinkColumnRenderer())
+ ->filter(new TextFilter('modification_log.username')),
+ GridViewColumn::for('action')
+ ->label('wcf.acp.modificationLog.action')
+ ->titleColumn()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof DatabaseObjectDecorator);
+ $log = $context->getDecoratedObject();
+ \assert($log instanceof ModificationLog);
+ $objectType = ObjectTypeCache::getInstance()->getObjectType($log->objectTypeID);
+ if (!$objectType) {
+ return '';
+ }
+
+ return WCF::getLanguage()->get(
+ 'wcf.acp.modificationLog.' . $objectType->objectType . '.' . $log->action
+ );
+ }
+ },
+ ])
+ ->filter(
+ new class($this->getAvailableActions()) implements IGridViewFilter {
+ public function __construct(private readonly array $options) {}
+
+ #[\Override]
+ public function getFormField(string $id, string $label): AbstractFormField
+ {
+ return SelectFormField::create($id)
+ ->label($label)
+ ->options($this->options, false, false);
+ }
+
+ #[\Override]
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+ {
+ if (\is_numeric($value)) {
+ $list->getConditionBuilder()->add(
+ "objectTypeID IN (SELECT objectTypeID FROM wcf1_object_type WHERE packageID = ?)",
+ [$value]
+ );
+ } else if (\preg_match('~^(?P<objectType>.+)\-(?P<action>[^\-]+)$~', $value, $matches)) {
+ $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
+ 'com.woltlab.wcf.modifiableContent',
+ $matches['objectType']
+ );
+ if (!$objectType) {
+ return;
+ }
+
+ $list->getConditionBuilder()->add(
+ "objectTypeID = ? AND action = ?",
+ [$objectType->objectTypeID, $matches['action']]
+ );
+ }
+ }
+
+ #[\Override]
+ public function matches(string $filterValue, string $rowValue): bool
+ {
+ throw new LogicException('unreachable');
+ }
+
+ #[\Override]
+ public function renderValue(string $value): string
+ {
+ if (\is_numeric($value)) {
+ return WCF::getLanguage()->get($this->options[$value]);
+ }
+
+ return \substr(WCF::getLanguage()->get($this->options[$value]), 24);
+ }
+ }
+ ),
+ GridViewColumn::for('affectedObject')
+ ->label('wcf.acp.modificationLog.affectedObject')
+ ->renderer([
+ new class extends DefaultColumnRenderer implements ILinkColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof IViewableModificationLog);
+ if ($context->getAffectedObject() === null) {
+ return WCF::getLanguage()->get('wcf.acp.modificationLog.affectedObject.unknown');
+ }
+
+ return \sprintf(
+ '<a href="%s">%s</a>',
+ StringUtil::encodeHTML($context->getAffectedObject()->getLink()),
+ StringUtil::encodeHTML($context->getAffectedObject()->getTitle())
+ );
+ }
+ },
+ ]),
+ GridViewColumn::for('time')
+ ->label('wcf.global.date')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer())
+ ->filter(new TimeFilter()),
+ ]);
+
+ $this->setSortField('time');
+ $this->setSortOrder('DESC');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canViewLog');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new ModificationLogList();
+ }
+
+ #[\Override]
+ public function getRows(): array
+ {
+ if (!isset($this->logItems)) {
+ $this->logItems = [];
+ $this->getObjectList()->readObjects();
+
+ $itemsPerType = [];
+ foreach ($this->getObjectList() as $modificationLog) {
+ if (!isset($itemsPerType[$modificationLog->objectTypeID])) {
+ $itemsPerType[$modificationLog->objectTypeID] = [];
+ }
+
+ $itemsPerType[$modificationLog->objectTypeID][] = $modificationLog;
+ }
+
+ if (!empty($itemsPerType)) {
+ foreach ($itemsPerType as $objectTypeID => $items) {
+ $objectType = ObjectTypeCache::getInstance()->getObjectType($objectTypeID);
+ if (!$objectType) {
+ continue;
+ }
+ $processor = $objectType->getProcessor();
+ if (!$processor) {
+ continue;
+ }
+ \assert($processor instanceof IExtendedModificationLogHandler);
+
+ $this->logItems = \array_merge(
+ $this->logItems,
+ $processor->processItems($items)
+ );
+ }
+ }
+
+ DatabaseObject::sort($this->logItems, $this->getSortField(), $this->getSortOrder());
+ }
+
+ return $this->logItems;
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new ModificationLogGridViewInitialized($this);
+ }
+
+ private function getAvailableActions(): array
+ {
+ $packages = $actions = $availableActions = [];
+
+ foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.modifiableContent') as $objectType) {
+ $processor = $objectType->getProcessor();
+ if ($processor === null) {
+ continue;
+ }
+ \assert($processor instanceof IExtendedModificationLogHandler);
+
+ if (!$processor->includeInLogList()) {
+ continue;
+ }
+
+ if (!isset($packages[$objectType->packageID])) {
+ $actions[$objectType->packageID] = [];
+ $packages[$objectType->packageID] = $objectType->getPackage();
+ }
+
+ foreach ($processor->getAvailableActions() as $action) {
+ $actions[$objectType->packageID]["{$objectType->objectType}-{$action}"]
+ = WCF::getLanguage()->get("wcf.acp.modificationLog.{$objectType->objectType}.{$action}");
+ }
+ }
+
+ foreach ($actions as &$actionsPerPackage) {
+ \asort($actionsPerPackage, \SORT_NATURAL);
+ }
+ \uasort($packages, static function (Package $a, Package $b) {
+ return \strnatcasecmp($a->package, $b->package);
+ });
+
+ foreach ($packages as $package) {
+ $availableActions[$package->packageID]
+ = WCF::getLanguage()->getDynamicVariable(
+ 'wcf.acp.modificationLog.action.allPackageActions',
+ ['package' => $package]
+ );
+
+ foreach ($actions[$package->packageID] as $actionName => $actionLabel) {
+ $availableActions[$actionName] = \str_repeat(' ', 4) . $actionLabel;
+ }
+ }
+
+ return $availableActions;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use wcf\acp\form\UserOptionEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\option\UserOption;
+use wcf\data\user\option\UserOptionList;
+use wcf\event\gridView\admin\UserOptionGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\DatabaseObjectListGridView;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\GridViewRowLink;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\interaction\admin\UserOptionInteractions;
+use wcf\system\interaction\Divider;
+use wcf\system\interaction\EditInteraction;
+use wcf\system\interaction\ToggleInteraction;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Grid view for the list of user options.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserOptionGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('optionID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('optionName')
+ ->label('wcf.global.name')
+ ->sortable()
+ ->titleColumn()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserOption);
+
+ return StringUtil::encodeHTML($context->getTitle());
+ }
+ }
+ ]),
+ GridViewColumn::for('categoryName')
+ ->label('wcf.global.category')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserOption);
+
+ return StringUtil::encodeHTML(
+ WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
+ );
+ }
+ }
+ ]),
+ GridViewColumn::for('optionType')
+ ->label('wcf.acp.user.option.optionType')
+ ->sortable(),
+ GridViewColumn::for('showOrder')
+ ->label('wcf.global.showOrder')
+ ->sortable()
+ ->renderer(new NumberColumnRenderer()),
+ ]);
+
+ $provider = new UserOptionInteractions();
+ $provider->addInteractions([
+ new Divider(),
+ new EditInteraction(UserOptionEditForm::class)
+ ]);
+ $this->setInteractionProvider($provider);
+ $this->addQuickInteraction(
+ new ToggleInteraction('enable', 'core/users/options/%s/enable', 'core/users/options/%s/disable')
+ );
+ $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
+ $this->setSortField('showOrder');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.user.canManageUserOption');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ $list = new UserOptionList();
+ $list->getConditionBuilder()->add(
+ "option_table.categoryName IN (
+ SELECT categoryName
+ FROM wcf" . WCF_N . "_user_option_category
+ WHERE parentCategoryName = ?
+ )",
+ ['profile']
+ );
+
+ return $list;
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new UserOptionGridViewInitialized($this);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\admin;
+
+use wcf\acp\form\UserRankEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\group\UserGroup;
+use wcf\data\user\rank\I18nUserRankList;
+use wcf\data\user\rank\UserRank;
+use wcf\event\gridView\admin\UserRankGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\DatabaseObjectListGridView;
+use wcf\system\gridView\filter\I18nTextFilter;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\GridViewColumn;
+use wcf\system\gridView\GridViewRowLink;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\interaction\admin\UserRankInteractions;
+use wcf\system\interaction\Divider;
+use wcf\system\interaction\EditInteraction;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Grid view for the list of user ranks.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserRankGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('rankID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('rankTitle')
+ ->label('wcf.acp.user.rank.title')
+ ->sortable(true, 'rankTitleI18n')
+ ->titleColumn()
+ ->filter(new I18nTextFilter())
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserRank);
+
+ return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
+ . StringUtil::encodeHTML($context->getTitle())
+ . '<span>';
+ }
+ }
+ ]),
+ GridViewColumn::for('rankImage')
+ ->label('wcf.acp.user.rank.image')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserRank);
+
+ return $context->rankImage ? $context->getImage() : '';
+ }
+ },
+ ]),
+ GridViewColumn::for('groupID')
+ ->label('wcf.user.group')
+ ->sortable()
+ ->filter(new SelectFilter($this->getAvailableUserGroups()))
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
+ }
+ },
+ ]),
+ GridViewColumn::for('requiredGender')
+ ->label('wcf.user.option.gender')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ if (!$value) {
+ return '';
+ }
+
+ return WCF::getLanguage()->get(match ($value) {
+ 1 => 'wcf.user.gender.male',
+ 2 => 'wcf.user.gender.female',
+ default => 'wcf.user.gender.other'
+ });
+ }
+ },
+ ]),
+ GridViewColumn::for('requiredPoints')
+ ->label('wcf.acp.user.rank.requiredPoints')
+ ->sortable()
+ ->renderer(new NumberColumnRenderer()),
+ ]);
+
+ $provider = new UserRankInteractions();
+ $provider->addInteractions([
+ new Divider(),
+ new EditInteraction(UserRankEditForm::class)
+ ]);
+ $this->setInteractionProvider($provider);
+ $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
+ $this->setSortField('rankTitle');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new I18nUserRankList();
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new UserRankGridViewInitialized($this);
+ }
+
+ private function getAvailableUserGroups(): array
+ {
+ $groups = [];
+ foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
+ $groups[$group->groupID] = $group->getName();
+ }
+
+ return $groups;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+
+/**
+ * Provides an abstract implementation of an interaction that can be applied to a DatabaseObject.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+abstract class AbstractInteraction implements IInteraction
+{
+ public function __construct(
+ protected readonly string $identifier,
+ protected readonly ?\Closure $isAvailableCallback = null
+ ) {}
+
+ #[\Override]
+ public function isAvailable(DatabaseObject $object): bool
+ {
+ if ($this->isAvailableCallback === null) {
+ return true;
+ }
+
+ return ($this->isAvailableCallback)($object);
+ }
+
+ #[\Override]
+ public function renderInitialization(string $containerId): ?string
+ {
+ return null;
+ }
+
+ #[\Override]
+ public function getIdentifier(): string
+ {
+ return $this->identifier;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Provides an abstract implementation of a provider that provides interactions
+ * that can be applied to a specific type of DatabaseObject.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+abstract class AbstractInteractionProvider implements IInteractionProvider
+{
+ /**
+ * @var (IInteraction|Divider)[]
+ */
+ private array $interactions = [];
+
+ #[\Override]
+ public function getInteractions(): array
+ {
+ return $this->interactions;
+ }
+
+ #[\Override]
+ public function addInteraction(IInteraction|Divider $interaction): void
+ {
+ $this->interactions[] = $interaction;
+ }
+
+ #[\Override]
+ public function addInteractions(array $interactions): void
+ {
+ foreach ($interactions as $interaction) {
+ $this->addInteraction($interaction);
+ }
+ }
+
+ #[\Override]
+ public function addInteractionBefore(IInteraction|Divider $interaction, string $beforeID): void
+ {
+ $position = -1;
+
+ foreach ($this->getInteractions() as $key => $existingInteraction) {
+ if ($existingInteraction->getIdentifier() === $beforeID) {
+ $position = $key;
+ break;
+ }
+ }
+
+ if ($position === -1) {
+ throw new \InvalidArgumentException("Invalid interaction id '{$beforeID}' given.");
+ }
+
+ array_splice($this->interactions, $position, 0, [
+ $interaction,
+ ]);
+ }
+
+ #[\Override]
+ public function addInteractionAfter(IInteraction|Divider $interaction, string $afterID): void
+ {
+ $position = -1;
+
+ foreach ($this->getInteractions() as $key => $existingInteraction) {
+ if ($existingInteraction->getIdentifier() === $afterID) {
+ $position = $key;
+ break;
+ }
+ }
+
+ if ($position === -1) {
+ throw new \InvalidArgumentException("Invalid interaction id '{$afterID}' given.");
+ }
+
+ array_splice($this->interactions, $position + 1, 0, [
+ $interaction,
+ ]);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Represents a delete interaction.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class DeleteInteraction extends RpcInteraction
+{
+ public function __construct(
+ string $endpoint,
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct(
+ 'delete',
+ $endpoint,
+ 'wcf.global.button.delete',
+ InteractionConfirmationType::Delete,
+ '',
+ $isAvailableCallback
+ );
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Represents a separator in an interaction context menu.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class Divider {}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Represents an interaction that links to an edit form controller.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class EditInteraction extends LinkInteraction
+{
+ public function __construct(
+ string $controllerClass,
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct('edit', $controllerClass, 'wcf.global.button.edit', $isAvailableCallback);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents an interaction that call a form builder action.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class FormBuilderDialogInteraction extends AbstractInteraction
+{
+ public function __construct(
+ string $identifier,
+ protected readonly string $endpoint,
+ protected readonly string|\Closure $languageItem,
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($identifier, $isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(DatabaseObject $object): string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+
+ if (\is_string($this->languageItem)) {
+ $label = WCF::getLanguage()->get($this->languageItem);
+ } else {
+ $label = ($this->languageItem)($object);
+ }
+
+ $endpoint = StringUtil::encodeHTML(
+ \sprintf($this->endpoint, $object->getObjectID())
+ );
+
+ return <<<HTML
+ <button
+ type="button"
+ data-interaction="{$identifier}"
+ data-endpoint="{$endpoint}"
+ >
+ {$label}
+ </button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(string $containerId): ?string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+ $containerId = StringUtil::encodeJS($containerId);
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Interaction/FormBuilderDialog'], ({ setup }) => {
+ setup('{$identifier}', document.getElementById('{$containerId}'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents an interaction that can be applied to a DatabaseObject.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+interface IInteraction
+{
+ /**
+ * Renders the interaction for the given object.
+ */
+ public function render(DatabaseObject $object): string;
+
+ /**
+ * Renders the initialization code for this interaction.
+ */
+ public function renderInitialization(string $containerId): ?string;
+
+ /**
+ * Returns true if this interaction is available for the given object
+ */
+ public function isAvailable(DatabaseObject $object): bool;
+
+ /**
+ * Returns the identifier of this interaction.
+ */
+ public function getIdentifier(): string;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Represents a provider that provides interactions that can be applied to a specific type of DatabaseObject.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+interface IInteractionProvider
+{
+ /**
+ * Returns the interactions provided by this provider.
+ * @return (IInteraction|Divider)[]
+ */
+ public function getInteractions(): array;
+
+ /**
+ * Adds the given interaction to the provider.
+ */
+ public function addInteraction(IInteraction|Divider $interaction): void;
+
+ /**
+ * Adds the given interactions to the provider.
+ * @param (IInteraction|Divider)[] $interactions
+ */
+ public function addInteractions(array $interactions): void;
+
+ /**
+ * Adds a new interaction at the position before the given id.
+ */
+ public function addInteractionBefore(IInteraction|Divider $interaction, string $beforeID): void;
+
+ /**
+ * Adds a new interaction at the position after the given id.
+ */
+ public function addInteractionAfter(IInteraction|Divider $interaction, string $afterID): void;
+
+ /**
+ * Returns the class name of the object that the interactions can be applied to.
+ */
+ public function getObjectClassName(): string;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+/**
+ * Represents a confirmation type used in interactions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+enum InteractionConfirmationType
+{
+ case None;
+ case SoftDelete;
+ case SoftDeleteWithReason;
+ case Restore;
+ case Delete;
+ case Custom;
+
+ public function toString(): string
+ {
+ return match ($this) {
+ self::None => 'None',
+ self::SoftDelete => 'SoftDelete',
+ self::SoftDeleteWithReason => 'SoftDeleteWithReason',
+ self::Restore => 'Restore',
+ self::Delete => 'Delete',
+ self::Custom => 'Custom',
+ };
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+use wcf\system\WCF;
+
+class InteractionContextMenuView
+{
+ public function __construct(
+ protected readonly IInteractionProvider $provider
+ ) {}
+
+ public function renderContextMenuOptions(DatabaseObject $object): string
+ {
+ $html = '';
+
+ $interactions = $this->getInteractionsForObject($object);
+
+ foreach ($interactions as $interaction) {
+ if ($interaction instanceof Divider) {
+ $html .= '<li class="dropdownDivider"></li>';
+ } else {
+ $html .= '<li>' . $interaction->render($object) . '</li>';
+ }
+ }
+
+ return $html;
+ }
+
+ public function renderButton(DatabaseObject $object): string
+ {
+ return WCF::getTPL()->fetch(
+ 'shared_interactionButton',
+ 'wcf',
+ ['contextMenuOptions' => $this->renderContextMenuOptions($object)],
+ true
+ );
+ }
+
+ /**
+ * Renders the initialization code for the interactions.
+ */
+ public function renderInitialization(string $containerId): string
+ {
+ return implode(
+ "\n",
+ \array_map(
+ fn($interaction) => $interaction->renderInitialization($containerId),
+ \array_filter(
+ $this->getInteractions(),
+ fn(IInteraction|Divider $interaction) => $interaction instanceof IInteraction
+ )
+ )
+ );
+ }
+
+ public function getInteractionsForObject(DatabaseObject $object): array
+ {
+ $interactions = \array_filter(
+ $this->getInteractions(),
+ fn(IInteraction|Divider $interaction) => $interaction instanceof Divider || $interaction->isAvailable($object)
+ );
+
+ return $this->removeObsoleteDividers($interactions);
+ }
+
+ public function getInteractions(): array
+ {
+ return $this->provider->getInteractions();
+ }
+
+ private function removeObsoleteDividers(array $interactions): array
+ {
+ $previousElementIsDivider = true;
+ $interactions = \array_filter(
+ $interactions,
+ static function (IInteraction|Divider $interaction) use (&$previousElementIsDivider) {
+ if ($interaction instanceof Divider) {
+ if ($previousElementIsDivider) {
+ return false;
+ }
+
+ $previousElementIsDivider = true;
+ } else {
+ $previousElementIsDivider = false;
+ }
+
+ return true;
+ }
+ );
+
+ $lastKey = \array_key_last($interactions);
+ if ($lastKey !== null && $interactions[$lastKey] instanceof Divider) {
+ \array_pop($interactions);
+ }
+
+ return $interactions;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+use wcf\data\ITitledObject;
+use wcf\system\interaction\AbstractInteraction;
+use wcf\system\interaction\InteractionConfirmationType;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents an interaction that executes a dbo action.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @deprecated 6.2 DBO actions are considered outdated and should be migrated to RPC endpoints.
+ */
+class LegacyDboInteraction extends AbstractInteraction
+{
+ public function __construct(
+ string $identifier,
+ protected readonly string $className,
+ protected readonly string $actionName,
+ protected readonly string|\Closure $languageItem,
+ protected readonly InteractionConfirmationType $confirmationType = InteractionConfirmationType::None,
+ protected readonly string|\Closure $confirmationMessage = '',
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($identifier, $isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(DatabaseObject $object): string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+
+ if (\is_string($this->languageItem)) {
+ $label = WCF::getLanguage()->get($this->languageItem);
+ } else {
+ $label = ($this->languageItem)($object);
+ }
+
+ if (\is_string($this->confirmationMessage)) {
+ $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage);
+ } else {
+ $confirmationMessage = ($this->confirmationMessage)($object);
+ }
+
+ if ($object instanceof ITitledObject) {
+ $objectName = StringUtil::encodeHTML($object->getTitle());
+ } else {
+ $objectName = '';
+ }
+
+ $className = StringUtil::encodeHTML($this->className);
+ $actionName = StringUtil::encodeHTML($this->actionName);
+
+ return <<<HTML
+ <button
+ type="button"
+ data-interaction="{$identifier}"
+ data-object-name="{$objectName}"
+ data-class-name="{$className}"
+ data-action-name="{$actionName}"
+ data-confirmation-type="{$this->confirmationType->toString()}"
+ data-confirmation-message="{$confirmationMessage}"
+ >
+ {$label}
+ </button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(string $containerId): ?string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+ $containerId = StringUtil::encodeJS($containerId);
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Interaction/LegacyDboAction'], ({ setup }) => {
+ setup('{$identifier}', document.getElementById('{$containerId}'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents an interaction that links to a given controller.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class LinkInteraction extends AbstractInteraction
+{
+ public function __construct(
+ string $identifier,
+ protected readonly string $controllerClass,
+ protected readonly string|\Closure $languageItem,
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($identifier, $isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(DatabaseObject $object): string
+ {
+ $href = LinkHandler::getInstance()->getControllerLink(
+ $this->controllerClass,
+ ['object' => $object]
+ );
+
+ if (\is_string($this->languageItem)) {
+ $title = WCF::getLanguage()->get($this->languageItem);
+ } else {
+ $title = ($this->languageItem)($object);
+ }
+
+ return \sprintf('<a href="%s">%s</a>', StringUtil::encodeHTML($href), $title);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\data\ITitledObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents an interaction that call a rpc endpoint.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class RpcInteraction extends AbstractInteraction
+{
+ public function __construct(
+ string $identifier,
+ protected readonly string $endpoint,
+ protected readonly string|\Closure $languageItem,
+ protected readonly InteractionConfirmationType $confirmationType = InteractionConfirmationType::None,
+ protected readonly string|\Closure $confirmationMessage = '',
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($identifier, $isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(DatabaseObject $object): string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+
+ if (\is_string($this->languageItem)) {
+ $label = WCF::getLanguage()->get($this->languageItem);
+ } else {
+ $label = ($this->languageItem)($object);
+ }
+
+ if (\is_string($this->confirmationMessage)) {
+ $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage);
+ } else {
+ $confirmationMessage = ($this->confirmationMessage)($object);
+ }
+
+ $endpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->endpoint, $object->getObjectID())
+ );
+
+ if ($object instanceof ITitledObject) {
+ $objectName = StringUtil::encodeHTML($object->getTitle());
+ } else {
+ $objectName = '';
+ }
+
+ return <<<HTML
+ <button
+ type="button"
+ data-interaction="{$identifier}"
+ data-object-name="{$objectName}"
+ data-endpoint="{$endpoint}"
+ data-confirmation-type="{$this->confirmationType->toString()}"
+ data-confirmation-message="{$confirmationMessage}"
+ >
+ {$label}
+ </button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(string $containerId): ?string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+ $containerId = StringUtil::encodeJS($containerId);
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Interaction/Rpc'], ({ setup }) => {
+ setup('{$identifier}', document.getElementById('{$containerId}'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\data\DatabaseObject;
+use wcf\system\WCF;
+
+class StandaloneInteractionContextMenuView extends InteractionContextMenuView
+{
+ public function __construct(
+ IInteractionProvider $provider,
+ protected readonly DatabaseObject $object,
+ protected readonly string $redirectUrl
+ ) {
+ parent::__construct($provider);
+ }
+
+ public function render(): string
+ {
+ return WCF::getTPL()->fetch(
+ 'shared_standaloneInteractionButton',
+ 'wcf',
+ [
+ 'contextMenuOptions' => $this->renderContextMenuOptions($this->object),
+ 'initializationCode' => $this->renderInitialization($this->getContainerID()),
+ 'containerID' => $this->getContainerID(),
+ 'providerClassName' => \get_class($this->provider),
+ 'objectID' => $this->object->getObjectID(),
+ 'redirectUrl' => $this->redirectUrl,
+ ],
+ true
+ );
+
+ return '';
+ }
+
+ public function getContainerID(): string
+ {
+ $classNamePieces = \explode('\\', \get_class($this->object));
+
+ return \implode('-', $classNamePieces) . '-' . $this->object->getObjectID();
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction;
+
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents a toggle interaction.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+class ToggleInteraction extends AbstractInteraction
+{
+ public function __construct(
+ string $identifier,
+ private readonly string $enableEndpoint,
+ private readonly string $disableEndpoint,
+ private readonly string $propertyName = 'isDisabled',
+ private readonly bool $propertyIsDisabledState = true,
+ ?\Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($identifier, $isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(DatabaseObject $object): string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+
+ $enableEndpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->enableEndpoint, $object->getObjectID())
+ );
+ $disableEndpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->disableEndpoint, $object->getObjectID())
+ );
+
+ $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
+ $checked = (!$object->{$this->propertyName} && $this->propertyIsDisabledState)
+ || ($object->{$this->propertyName} && !$this->propertyIsDisabledState) ? 'checked' : '';
+
+ return <<<HTML
+ <woltlab-core-toggle-button
+ data-interaction="{$identifier}"
+ aria-label="{$ariaLabel}"
+ data-enable-endpoint="{$enableEndpoint}"
+ data-disable-endpoint="{$disableEndpoint}"
+ {$checked}
+ ></woltlab-core-toggle-button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(string $containerId): ?string
+ {
+ $identifier = StringUtil::encodeJS($this->getIdentifier());
+ $containerId = StringUtil::encodeJS($containerId);
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Interaction/Toggle'], ({ setup }) => {
+ setup('{$identifier}', document.getElementById('{$containerId}'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction\admin;
+
+use wcf\data\user\option\UserOption;
+use wcf\event\interaction\admin\UserOptionInteractionCollecting;
+use wcf\system\event\EventHandler;
+use wcf\system\interaction\AbstractInteractionProvider;
+use wcf\system\interaction\DeleteInteraction;
+
+/**
+ * Interaction provider for user options.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserOptionInteractions extends AbstractInteractionProvider
+{
+ public function __construct()
+ {
+ $this->addInteractions([
+ new DeleteInteraction('core/users/options/%s', static fn(UserOption $object) => $object->canDelete())
+ ]);
+
+ EventHandler::getInstance()->fire(
+ new UserOptionInteractionCollecting($this)
+ );
+ }
+
+ #[\Override]
+ public function getObjectClassName(): string
+ {
+ return UserOption::class;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\interaction\admin;
+
+use wcf\data\user\rank\UserRank;
+use wcf\event\interaction\admin\UserRankInteractionCollecting;
+use wcf\system\event\EventHandler;
+use wcf\system\interaction\AbstractInteractionProvider;
+use wcf\system\interaction\DeleteInteraction;
+
+/**
+ * Interaction provider for user ranks.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+final class UserRankInteractions extends AbstractInteractionProvider
+{
+ public function __construct()
+ {
+ $this->addInteractions([
+ new DeleteInteraction('core/users/ranks/%s'),
+ ]);
+
+ EventHandler::getInstance()->fire(
+ new UserRankInteractionCollecting($this)
+ );
+ }
+
+ #[\Override]
+ public function getObjectClassName(): string
+ {
+ return UserRank::class;
+ }
+}