From: Marcel Werk Date: Thu, 9 Jan 2025 14:30:24 +0000 (+0100) Subject: Migrate grid view actions into interactions, that can be used outside of the grid... X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=fa048036c158a2652878cb5224d5728c9267af1c;p=GitHub%2FWoltLab%2FWCF.git Migrate grid view actions into interactions, that can be used outside of the grid views --- diff --git a/com.woltlab.wcf/templates/shared_gridView.tpl b/com.woltlab.wcf/templates/shared_gridView.tpl index 1330fe350d..111d75f918 100644 --- a/com.woltlab.wcf/templates/shared_gridView.tpl +++ b/com.woltlab.wcf/templates/shared_gridView.tpl @@ -37,7 +37,7 @@ {/if} {/foreach} - {if $view->hasActions()} + {if $view->hasInteractions()} {/if} @@ -72,4 +72,6 @@ ); }); -{unsafe:$view->renderActionInitialization()} +{if $view->hasInteractions()} + {unsafe:$view->renderInteractionInitialization()} +{/if} diff --git a/com.woltlab.wcf/templates/shared_gridViewRows.tpl b/com.woltlab.wcf/templates/shared_gridViewRows.tpl index d3f741d906..2aa6fb27fc 100644 --- a/com.woltlab.wcf/templates/shared_gridViewRows.tpl +++ b/com.woltlab.wcf/templates/shared_gridViewRows.tpl @@ -6,38 +6,11 @@ {unsafe:$view->renderColumn($column, $row)} {/foreach} - {if $view->hasActions()} + {if $view->hasInteractions()}
- {foreach from=$view->getQuickActions() item='action'} - {unsafe:$view->renderAction($action, $row)} - {/foreach} - - {if $view->hasDropdownActions()} - {hascontent} - - {hascontentelse} - - {/hascontent} - {/if} + {unsafe:$view->renderQuickInteractions($row)} + {unsafe:$view->renderInteractionContextMenuButton($row)}
{/if} diff --git a/com.woltlab.wcf/templates/shared_interactionButton.tpl b/com.woltlab.wcf/templates/shared_interactionButton.tpl new file mode 100644 index 0000000000..1035e86ce7 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_interactionButton.tpl @@ -0,0 +1,15 @@ +{if $contextMenuOptions} + +{else} + +{/if} diff --git a/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl b/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl new file mode 100644 index 0000000000..361a62c451 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl @@ -0,0 +1,22 @@ + + + + +{unsafe:$initializationCode} diff --git a/ts/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.ts b/ts/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.ts new file mode 100644 index 0000000000..c467dd1dbf --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.ts @@ -0,0 +1,24 @@ +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> { + 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); +} diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index 9162951dda..e36f35ee52 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -3,6 +3,7 @@ import { getRows } from "../Api/Gridviews/GetRows"; 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"; @@ -47,7 +48,7 @@ export class GridView { this.#initPagination(); this.#initSorting(); - this.#initActions(); + this.#initInteractions(); this.#initFilters(); this.#initEventListeners(); @@ -128,7 +129,6 @@ export class GridView { DomChangeListener.trigger(); this.#renderFilters(response.filterLabels); - this.#initActions(); } async #refreshRow(row: HTMLElement): Promise { @@ -166,18 +166,18 @@ export class GridView { window.history.pushState({}, document.title, url.toString()); } - #initActions(): void { - this.#table.querySelectorAll("tbody tr").forEach((row) => { - row.querySelectorAll(".gridViewActions").forEach((element) => { + #initInteractions(): void { + wheneverFirstSeen(`#${this.#table.id} tbody tr`, (row) => { + row.querySelectorAll(".dropdownToggle").forEach((element) => { let dropdown = UiDropdownSimple.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", { + new CustomEvent("interaction", { detail: element.dataset, bubbles: true, }), @@ -295,5 +295,9 @@ export class GridView { this.#table.addEventListener("refresh", (event) => { void this.#refreshRow(event.target as HTMLElement); }); + + this.#table.addEventListener("remove", (event) => { + (event.target as HTMLElement).remove(); + }); } } diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Confirmation.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/Confirmation.ts deleted file mode 100644 index 69e5943b78..0000000000 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Confirmation.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 { - 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, - }; -} diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts deleted file mode 100644 index ed32caade3..0000000000 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject"; -import { confirmationFactory } from "../../Confirmation"; -import * as UiNotification from "WoltLabSuite/Core/Ui/Notification"; - -async function handleDelete(row: HTMLTableRowElement, objectName: string, endpoint: string): Promise { - const confirmationResult = await confirmationFactory().delete(objectName ? 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); - } - }); -} diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.ts deleted file mode 100644 index a242bf73ca..0000000000 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Handles execution of DBO actions within grid views. - * - * @author Marcel Werk - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - * @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 { - 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, - ); - } - }); -} diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts deleted file mode 100644 index 6c09a62bfc..0000000000 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 { - 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, - ); - } - }); -} diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Toggle.ts b/ts/WoltLabSuite/Core/Component/GridView/Action/Toggle.ts deleted file mode 100644 index 9b4032399f..0000000000 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Toggle.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; -import { postObject } from "WoltLabSuite/Core/Api/PostObject"; - -async function handleToggle(checked: boolean, enableEndpoint: string, disableEndpoint: string): Promise { - 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!, - ); - }); - }); -} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts b/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts new file mode 100644 index 0000000000..69e5943b78 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts @@ -0,0 +1,51 @@ +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 { + 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, + }; +} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts b/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts new file mode 100644 index 0000000000..0f5a5b13e0 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts @@ -0,0 +1,27 @@ +import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification"; +import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; + +async function handleFormBuilderDialogAction(element: HTMLElement, endpoint: string): Promise { + 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); + } + }); +} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts b/ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts new file mode 100644 index 0000000000..4b04330477 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts @@ -0,0 +1,67 @@ +/** + * Handles execution of DBO actions within grid views. + * + * @author Marcel Werk + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @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 { + 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, + ); + } + }); +} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts new file mode 100644 index 0000000000..9b3edd8d37 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts @@ -0,0 +1,66 @@ +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 { + 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, + ); + } + }); +} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts b/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts new file mode 100644 index 0000000000..463e93264f --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts @@ -0,0 +1,71 @@ +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 { + 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(".dropdownToggle"); + if (!button) { + return undefined; + } + + let dropdown = UiDropdownSimple.getDropdownMenu(button.dataset.target!); + if (!dropdown) { + dropdown = button.closest(".dropdown")!.querySelector(".dropdownMenu")!; + } + + return dropdown; + } + + #initInteractions(): void { + this.#getDropdownMenu() + ?.querySelectorAll("[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; + }); + } +} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts b/ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts new file mode 100644 index 0000000000..d89d636f26 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts @@ -0,0 +1,18 @@ +import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; +import { postObject } from "WoltLabSuite/Core/Api/PostObject"; + +async function handleToggle(checked: boolean, enableEndpoint: string, disableEndpoint: string): Promise { + 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!, + ); + }); + }); +} diff --git a/wcfsetup/install/files/acp/templates/userRankAdd.tpl b/wcfsetup/install/files/acp/templates/userRankAdd.tpl index 78c53ec55d..cc6f7d05dd 100644 --- a/wcfsetup/install/files/acp/templates/userRankAdd.tpl +++ b/wcfsetup/install/files/acp/templates/userRankAdd.tpl @@ -31,8 +31,11 @@ diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.js new file mode 100644 index 0000000000..208f2b0d60 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.js @@ -0,0 +1,18 @@ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.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); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js index d2ac3e8f5d..69ad1b10fd 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js @@ -1,4 +1,4 @@ -define(["require", "exports", "tslib", "../Api/Gridviews/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; @@ -36,7 +36,7 @@ define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridvi this.#gridViewParameters = gridViewParameters; this.#initPagination(); this.#initSorting(); - this.#initActions(); + this.#initInteractions(); this.#initFilters(); this.#initEventListeners(); window.addEventListener("popstate", () => { @@ -94,7 +94,6 @@ define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridvi } 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(); @@ -125,16 +124,16 @@ define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridvi } 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, })); @@ -229,6 +228,9 @@ define(["require", "exports", "tslib", "../Api/Gridviews/GetRow", "../Api/Gridvi this.#table.addEventListener("refresh", (event) => { void this.#refreshRow(event.target); }); + this.#table.addEventListener("remove", (event) => { + event.target.remove(); + }); } } exports.GridView = GridView; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Confirmation.js deleted file mode 100644 index 7920ecc273..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Confirmation.js +++ /dev/null @@ -1,41 +0,0 @@ -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, - }; - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js deleted file mode 100644 index f22c23f8c6..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js +++ /dev/null @@ -1,26 +0,0 @@ -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); - } - }); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.js deleted file mode 100644 index 1a917fe8ec..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Handles execution of DBO actions within grid views. - * - * @author Marcel Werk - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - * @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); - } - }); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Rpc.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Rpc.js deleted file mode 100644 index 4cf316af53..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Rpc.js +++ /dev/null @@ -1,40 +0,0 @@ -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); - } - }); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Toggle.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Toggle.js deleted file mode 100644 index af05f9c295..0000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Toggle.js +++ /dev/null @@ -1,15 +0,0 @@ -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); - }); - }); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js new file mode 100644 index 0000000000..7920ecc273 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js @@ -0,0 +1,41 @@ +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, + }; + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js new file mode 100644 index 0000000000..9a2c7e2797 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js @@ -0,0 +1,23 @@ +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); + } + }); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js new file mode 100644 index 0000000000..87821a3e93 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js @@ -0,0 +1,46 @@ +/** + * Handles execution of DBO actions within grid views. + * + * @author Marcel Werk + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @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); + } + }); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js new file mode 100644 index 0000000000..458293bc29 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js @@ -0,0 +1,45 @@ +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); + } + }); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js new file mode 100644 index 0000000000..40e8f1066f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js @@ -0,0 +1,61 @@ +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; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js new file mode 100644 index 0000000000..191b7bd8e7 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js @@ -0,0 +1,15 @@ +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); + }); + }); + } +}); diff --git a/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php index 5c5df25bff..9f2c044dd1 100644 --- a/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php @@ -2,12 +2,16 @@ 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; /** @@ -136,6 +140,11 @@ class UserRankEditForm extends UserRankAddForm 'rankID' => $this->rankID, 'rank' => $this->rank, 'action' => 'edit', + 'interactionContextMenu' => new StandaloneInteractionContextMenuView( + new UserRankInteractions(), + $this->rank, + LinkHandler::getInstance()->getControllerLink(UserRankListPage::class) + ), ]); } } diff --git a/wcfsetup/install/files/lib/acp/page/ACPSessionLogListPage.class.php b/wcfsetup/install/files/lib/acp/page/ACPSessionLogListPage.class.php index 0053e4ea7f..b22fecb53d 100755 --- a/wcfsetup/install/files/lib/acp/page/ACPSessionLogListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/ACPSessionLogListPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; 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. diff --git a/wcfsetup/install/files/lib/acp/page/CronjobLogListPage.class.php b/wcfsetup/install/files/lib/acp/page/CronjobLogListPage.class.php index ecdebc25be..501d7a2526 100755 --- a/wcfsetup/install/files/lib/acp/page/CronjobLogListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/CronjobLogListPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; use wcf\page\AbstractGridViewPage; use wcf\system\gridView\AbstractGridView; -use wcf\system\gridView\CronjobLogGridView; +use wcf\system\gridView\admin\CronjobLogGridView; /** * Shows cronjob log information. diff --git a/wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php b/wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php index 5fd9ca0d0e..a7d2eabbe1 100644 --- a/wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; 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; /** diff --git a/wcfsetup/install/files/lib/acp/page/ModificationLogListPage.class.php b/wcfsetup/install/files/lib/acp/page/ModificationLogListPage.class.php index 4f6bf9e6ba..b535d40019 100644 --- a/wcfsetup/install/files/lib/acp/page/ModificationLogListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/ModificationLogListPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; 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. diff --git a/wcfsetup/install/files/lib/acp/page/UserOptionListPage.class.php b/wcfsetup/install/files/lib/acp/page/UserOptionListPage.class.php index b1b1ad9882..406aeeb15e 100644 --- a/wcfsetup/install/files/lib/acp/page/UserOptionListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/UserOptionListPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; 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. diff --git a/wcfsetup/install/files/lib/acp/page/UserRankListPage.class.php b/wcfsetup/install/files/lib/acp/page/UserRankListPage.class.php index f2d80bf036..65a464ff53 100644 --- a/wcfsetup/install/files/lib/acp/page/UserRankListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/UserRankListPage.class.php @@ -4,7 +4,7 @@ namespace wcf\acp\page; use wcf\page\AbstractGridViewPage; use wcf\system\gridView\AbstractGridView; -use wcf\system\gridView\UserRankGridView; +use wcf\system\gridView\admin\UserRankGridView; /** * Lists available user ranks. diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index c4410b5469..7fb3872917 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -143,6 +143,7 @@ return static function (): void { $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()); } ); diff --git a/wcfsetup/install/files/lib/event/gridView/ACPSessionLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/ACPSessionLogGridViewInitialized.class.php deleted file mode 100644 index 43f1430006..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/ACPSessionLogGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class ACPSessionLogGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly ACPSessionLogGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/CronjobLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/CronjobLogGridViewInitialized.class.php deleted file mode 100644 index 53380598e2..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/CronjobLogGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class CronjobLogGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly CronjobLogGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/ExceptionLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/ExceptionLogGridViewInitialized.class.php deleted file mode 100644 index 3a8fa46f3b..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/ExceptionLogGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class ExceptionLogGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly ExceptionLogGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/ModificationLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/ModificationLogGridViewInitialized.class.php deleted file mode 100644 index 5333140766..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/ModificationLogGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class ModificationLogGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly ModificationLogGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/UserOptionGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/UserOptionGridViewInitialized.class.php deleted file mode 100644 index a5c638cd1c..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/UserOptionGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class UserOptionGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly UserOptionGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/UserRankGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/UserRankGridViewInitialized.class.php deleted file mode 100644 index 66f739a9c8..0000000000 --- a/wcfsetup/install/files/lib/event/gridView/UserRankGridViewInitialized.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -final class UserRankGridViewInitialized implements IPsr14Event -{ - public function __construct(public readonly UserRankGridView $gridView) {} -} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/ACPSessionLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/ACPSessionLogGridViewInitialized.class.php new file mode 100644 index 0000000000..61d2f9a2f7 --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/ACPSessionLogGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class ACPSessionLogGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly ACPSessionLogGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/CronjobLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/CronjobLogGridViewInitialized.class.php new file mode 100644 index 0000000000..a9b1bf8bb7 --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/CronjobLogGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class CronjobLogGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly CronjobLogGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/ExceptionLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/ExceptionLogGridViewInitialized.class.php new file mode 100644 index 0000000000..146c46befe --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/ExceptionLogGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class ExceptionLogGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly ExceptionLogGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/ModificationLogGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/ModificationLogGridViewInitialized.class.php new file mode 100644 index 0000000000..fb690fdb17 --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/ModificationLogGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class ModificationLogGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly ModificationLogGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/UserOptionGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/UserOptionGridViewInitialized.class.php new file mode 100644 index 0000000000..87d283294a --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/UserOptionGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class UserOptionGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly UserOptionGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/gridView/admin/UserRankGridViewInitialized.class.php b/wcfsetup/install/files/lib/event/gridView/admin/UserRankGridViewInitialized.class.php new file mode 100644 index 0000000000..05427ef0ba --- /dev/null +++ b/wcfsetup/install/files/lib/event/gridView/admin/UserRankGridViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class UserRankGridViewInitialized implements IPsr14Event +{ + public function __construct(public readonly UserRankGridView $gridView) {} +} diff --git a/wcfsetup/install/files/lib/event/interaction/admin/UserOptionInteractionCollecting.class.php b/wcfsetup/install/files/lib/event/interaction/admin/UserOptionInteractionCollecting.class.php new file mode 100644 index 0000000000..69c898f8a9 --- /dev/null +++ b/wcfsetup/install/files/lib/event/interaction/admin/UserOptionInteractionCollecting.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class UserOptionInteractionCollecting implements IPsr14Event +{ + public function __construct(public readonly UserOptionInteractions $provider) {} +} diff --git a/wcfsetup/install/files/lib/event/interaction/admin/UserRankInteractionCollecting.class.php b/wcfsetup/install/files/lib/event/interaction/admin/UserRankInteractionCollecting.class.php new file mode 100644 index 0000000000..4d2e4c9a47 --- /dev/null +++ b/wcfsetup/install/files/lib/event/interaction/admin/UserRankInteractionCollecting.class.php @@ -0,0 +1,19 @@ + + * @since 6.2 + */ +final class UserRankInteractionCollecting implements IPsr14Event +{ + public function __construct(public readonly UserRankInteractions $provider) {} +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/interactions/GetContextMenuOptions.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/interactions/GetContextMenuOptions.class.php new file mode 100644 index 0000000000..5888e4f0e6 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/interactions/GetContextMenuOptions.class.php @@ -0,0 +1,59 @@ + + * @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, + ) {} +} diff --git a/wcfsetup/install/files/lib/system/gridView/ACPSessionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/ACPSessionLogGridView.class.php deleted file mode 100644 index 88c45dde84..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/ACPSessionLogGridView.class.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @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); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php b/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php index 71add3162b..aa48ff9fed 100644 --- a/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php @@ -4,9 +4,12 @@ namespace wcf\system\gridView; 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; @@ -27,9 +30,9 @@ abstract class AbstractGridView private array $columns = []; /** - * @var IGridViewAction[] + * @var IInteraction[] */ - private array $actions = []; + private array $quickInteractions = []; private GridViewRowLink $rowLink; private int $rowsPerPage = 20; @@ -39,6 +42,8 @@ abstract class AbstractGridView 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. @@ -156,65 +161,44 @@ abstract class AbstractGridView } /** - * 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; } /** @@ -255,25 +239,67 @@ abstract class AbstractGridView } /** - * 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() + )); } /** diff --git a/wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php deleted file mode 100644 index 5a9e78e261..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php +++ /dev/null @@ -1,134 +0,0 @@ - - * @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 '' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . ''; - } - 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 << - {$label} - - - - 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()); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php deleted file mode 100644 index 8bf76a93a2..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php +++ /dev/null @@ -1,162 +0,0 @@ - - * @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); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/ModificationLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/ModificationLogGridView.class.php deleted file mode 100644 index a646b72445..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/ModificationLogGridView.class.php +++ /dev/null @@ -1,269 +0,0 @@ - - * @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.+)\-(?P[^\-]+)$~', $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( - '%s', - 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; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php b/wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php deleted file mode 100644 index d4f32f3357..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @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); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php deleted file mode 100644 index b588cfa783..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php +++ /dev/null @@ -1,140 +0,0 @@ - - * @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 '' - . StringUtil::encodeHTML($context->getTitle()) - . ''; - } - } - ]), - 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; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php deleted file mode 100644 index 776b167648..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @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; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/ActionConfirmationType.class.php b/wcfsetup/install/files/lib/system/gridView/action/ActionConfirmationType.class.php deleted file mode 100644 index 7bc5e9998c..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/ActionConfirmationType.class.php +++ /dev/null @@ -1,25 +0,0 @@ - 'None', - self::SoftDelete => 'SoftDelete', - self::SoftDeleteWithReason => 'SoftDeleteWithReason', - self::Restore => 'Restore', - self::Delete => 'Delete', - self::Custom => 'Custom', - }; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php deleted file mode 100644 index 435e43479d..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @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 << - {$label} - - HTML; - } - - #[\Override] - public function renderInitialization(AbstractGridView $gridView): ?string - { - $id = StringUtil::encodeJS($gridView->getID()); - - return << - require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => { - setup(document.getElementById('{$id}_table')); - }); - - HTML; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php deleted file mode 100644 index 277a07caff..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @since 6.2 - */ -class EditAction extends LinkAction -{ - public function __construct( - string $controllerClass, - ?Closure $isAvailableCallback = null - ) { - parent::__construct($controllerClass, 'wcf.global.button.edit', $isAvailableCallback); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php deleted file mode 100644 index 3ca6c33397..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @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; -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/LegacyDboAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/LegacyDboAction.class.php deleted file mode 100644 index 288f155aa7..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/LegacyDboAction.class.php +++ /dev/null @@ -1,88 +0,0 @@ - - * @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 << - {$label} - - HTML; - } - - #[\Override] - public function renderInitialization(AbstractGridView $gridView): ?string - { - $id = StringUtil::encodeJS($gridView->getID()); - - return << - require(['WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction'], ({ setup }) => { - setup(document.getElementById('{$id}_table')); - }); - - HTML; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php deleted file mode 100644 index 2173117e1f..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @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('%s', StringUtil::encodeHTML($href), $title); - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/RpcAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/RpcAction.class.php deleted file mode 100644 index ba02b0a336..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/RpcAction.class.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @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 << - {$label} - - HTML; - } - - #[\Override] - public function renderInitialization(AbstractGridView $gridView): ?string - { - $id = StringUtil::encodeJS($gridView->getID()); - - return << - require(['WoltLabSuite/Core/Component/GridView/Action/Rpc'], ({ setup }) => { - setup(document.getElementById('{$id}_table')); - }); - - HTML; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php deleted file mode 100644 index fc07fa5e1a..0000000000 --- a/wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php +++ /dev/null @@ -1,75 +0,0 @@ - - * @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; - } - - #[\Override] - public function renderInitialization(AbstractGridView $gridView): ?string - { - $id = StringUtil::encodeJS($gridView->getID()); - - return << - require(['WoltLabSuite/Core/Component/GridView/Action/Toggle'], ({ setup }) => { - setup('{$id}_table'); - }); - - HTML; - } - - #[\Override] - public function isQuickAction(): bool - { - return true; - } -} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php new file mode 100644 index 0000000000..48eadf68c6 --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php @@ -0,0 +1,108 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/CronjobLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/CronjobLogGridView.class.php new file mode 100644 index 0000000000..4834adae49 --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/CronjobLogGridView.class.php @@ -0,0 +1,136 @@ + + * @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 '' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . ''; + } + 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 << + {$label} + + + + 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()); + } +} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ExceptionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ExceptionLogGridView.class.php new file mode 100644 index 0000000000..501897a06f --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/ExceptionLogGridView.class.php @@ -0,0 +1,165 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ModificationLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ModificationLogGridView.class.php new file mode 100644 index 0000000000..b16b230c19 --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/ModificationLogGridView.class.php @@ -0,0 +1,271 @@ + + * @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.+)\-(?P[^\-]+)$~', $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( + '%s', + 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; + } +} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/UserOptionGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/UserOptionGridView.class.php new file mode 100644 index 0000000000..a9600cfa68 --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/UserOptionGridView.class.php @@ -0,0 +1,118 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php new file mode 100644 index 0000000000..3544d398fa --- /dev/null +++ b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php @@ -0,0 +1,146 @@ + + * @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 '' + . StringUtil::encodeHTML($context->getTitle()) + . ''; + } + } + ]), + 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; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/AbstractInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/AbstractInteraction.class.php new file mode 100644 index 0000000000..db936dfeec --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/AbstractInteraction.class.php @@ -0,0 +1,43 @@ + + * @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; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/AbstractInteractionProvider.class.php b/wcfsetup/install/files/lib/system/interaction/AbstractInteractionProvider.class.php new file mode 100644 index 0000000000..bd3d40629e --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/AbstractInteractionProvider.class.php @@ -0,0 +1,82 @@ + + * @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, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php new file mode 100644 index 0000000000..caae8bd811 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php @@ -0,0 +1,28 @@ + + * @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 + ); + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/Divider.class.php b/wcfsetup/install/files/lib/system/interaction/Divider.class.php new file mode 100644 index 0000000000..77f642f7ce --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/Divider.class.php @@ -0,0 +1,13 @@ + + * @since 6.2 + */ +final class Divider {} diff --git a/wcfsetup/install/files/lib/system/interaction/EditInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/EditInteraction.class.php new file mode 100644 index 0000000000..325c41d81b --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/EditInteraction.class.php @@ -0,0 +1,21 @@ + + * @since 6.2 + */ +class EditInteraction extends LinkInteraction +{ + public function __construct( + string $controllerClass, + ?\Closure $isAvailableCallback = null + ) { + parent::__construct('edit', $controllerClass, 'wcf.global.button.edit', $isAvailableCallback); + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/FormBuilderDialogInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/FormBuilderDialogInteraction.class.php new file mode 100644 index 0000000000..e2b11ebf4d --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/FormBuilderDialogInteraction.class.php @@ -0,0 +1,68 @@ + + * @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 << + {$label} + + HTML; + } + + #[\Override] + public function renderInitialization(string $containerId): ?string + { + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); + + return << + require(['WoltLabSuite/Core/Component/Interaction/FormBuilderDialog'], ({ setup }) => { + setup('{$identifier}', document.getElementById('{$containerId}')); + }); + + HTML; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/IInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/IInteraction.class.php new file mode 100644 index 0000000000..71f4187e94 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/IInteraction.class.php @@ -0,0 +1,36 @@ + + * @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; +} diff --git a/wcfsetup/install/files/lib/system/interaction/IInteractionProvider.class.php b/wcfsetup/install/files/lib/system/interaction/IInteractionProvider.class.php new file mode 100644 index 0000000000..22e71a7b9c --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/IInteractionProvider.class.php @@ -0,0 +1,46 @@ + + * @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; +} diff --git a/wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php b/wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php new file mode 100644 index 0000000000..30854d42ed --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php @@ -0,0 +1,33 @@ + + * @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', + }; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/InteractionContextMenuView.class.php b/wcfsetup/install/files/lib/system/interaction/InteractionContextMenuView.class.php new file mode 100644 index 0000000000..c6e1a5ad74 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/InteractionContextMenuView.class.php @@ -0,0 +1,100 @@ +getInteractionsForObject($object); + + foreach ($interactions as $interaction) { + if ($interaction instanceof Divider) { + $html .= ''; + } else { + $html .= '
  • ' . $interaction->render($object) . '
  • '; + } + } + + 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; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php new file mode 100644 index 0000000000..ca956dc07a --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php @@ -0,0 +1,90 @@ + + * @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 << + {$label} + + HTML; + } + + #[\Override] + public function renderInitialization(string $containerId): ?string + { + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); + + return << + require(['WoltLabSuite/Core/Component/Interaction/LegacyDboAction'], ({ setup }) => { + setup('{$identifier}', document.getElementById('{$containerId}')); + }); + + HTML; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php new file mode 100644 index 0000000000..ff32cc6585 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php @@ -0,0 +1,45 @@ + + * @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('%s', StringUtil::encodeHTML($href), $title); + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php new file mode 100644 index 0000000000..d9ae46dc46 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php @@ -0,0 +1,89 @@ + + * @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 << + {$label} + + HTML; + } + + #[\Override] + public function renderInitialization(string $containerId): ?string + { + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); + + return << + require(['WoltLabSuite/Core/Component/Interaction/Rpc'], ({ setup }) => { + setup('{$identifier}', document.getElementById('{$containerId}')); + }); + + HTML; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuView.class.php b/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuView.class.php new file mode 100644 index 0000000000..bc729a16fc --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuView.class.php @@ -0,0 +1,43 @@ +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(); + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/ToggleInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/ToggleInteraction.class.php new file mode 100644 index 0000000000..c53e141d3f --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/ToggleInteraction.class.php @@ -0,0 +1,75 @@ + + * @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; + } + + #[\Override] + public function renderInitialization(string $containerId): ?string + { + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); + + return << + require(['WoltLabSuite/Core/Component/Interaction/Toggle'], ({ setup }) => { + setup('{$identifier}', document.getElementById('{$containerId}')); + }); + + HTML; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/admin/UserOptionInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/UserOptionInteractions.class.php new file mode 100644 index 0000000000..3fe595c20f --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/admin/UserOptionInteractions.class.php @@ -0,0 +1,37 @@ + + * @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; + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/admin/UserRankInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/UserRankInteractions.class.php new file mode 100644 index 0000000000..56630d5f5a --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/admin/UserRankInteractions.class.php @@ -0,0 +1,37 @@ + + * @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; + } +}