From fa048036c158a2652878cb5224d5728c9267af1c Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 9 Jan 2025 15:30:24 +0100 Subject: [PATCH] Migrate grid view actions into interactions, that can be used outside of the grid views --- com.woltlab.wcf/templates/shared_gridView.tpl | 6 +- .../templates/shared_gridViewRows.tpl | 33 +---- .../templates/shared_interactionButton.tpl | 15 ++ .../shared_standaloneInteractionButton.tpl | 22 +++ .../Api/Interactions/GetContextMenuOptions.ts | 24 ++++ ts/WoltLabSuite/Core/Component/GridView.ts | 18 ++- .../Core/Component/GridView/Action/Delete.ts | 28 ---- .../Action => Interaction}/Confirmation.ts | 0 .../Interaction/FormBuilderDialog.ts | 27 ++++ .../Action => Interaction}/LegacyDboAction.ts | 23 ++-- .../{GridView/Action => Interaction}/Rpc.ts | 27 ++-- .../Component/Interaction/StandaloneButton.ts | 71 ++++++++++ .../Action => Interaction}/Toggle.ts | 4 +- .../files/acp/templates/userRankAdd.tpl | 7 +- .../Api/Interactions/GetContextMenuOptions.js | 18 +++ .../WoltLabSuite/Core/Component/GridView.js | 18 +-- .../Core/Component/GridView/Action/Delete.js | 26 ---- .../Action => Interaction}/Confirmation.js | 0 .../Interaction/FormBuilderDialog.js | 23 ++++ .../Action => Interaction}/LegacyDboAction.js | 19 ++- .../{GridView/Action => Interaction}/Rpc.js | 21 +-- .../Component/Interaction/StandaloneButton.js | 61 ++++++++ .../Action => Interaction}/Toggle.js | 4 +- .../lib/acp/form/UserRankEditForm.class.php | 9 ++ .../acp/page/ACPSessionLogListPage.class.php | 2 +- .../lib/acp/page/CronjobLogListPage.class.php | 2 +- .../acp/page/ExceptionLogViewPage.class.php | 2 +- .../page/ModificationLogListPage.class.php | 2 +- .../lib/acp/page/UserOptionListPage.class.php | 2 +- .../lib/acp/page/UserRankListPage.class.php | 2 +- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + ...ACPSessionLogGridViewInitialized.class.php | 4 +- .../CronjobLogGridViewInitialized.class.php | 4 +- .../ExceptionLogGridViewInitialized.class.php | 4 +- ...dificationLogGridViewInitialized.class.php | 4 +- .../UserOptionGridViewInitialized.class.php | 4 +- .../UserRankGridViewInitialized.class.php | 4 +- .../UserOptionInteractionCollecting.class.php | 19 +++ .../UserRankInteractionCollecting.class.php | 19 +++ .../GetContextMenuOptions.class.php | 59 ++++++++ .../gridView/AbstractGridView.class.php | 130 +++++++++++------- .../gridView/action/AbstractAction.class.php | 43 ------ .../gridView/action/DeleteAction.class.php | 67 --------- .../gridView/action/EditAction.class.php | 23 ---- .../gridView/action/IGridViewAction.class.php | 36 ----- .../gridView/action/ToggleAction.class.php | 75 ---------- .../ACPSessionLogGridView.class.php | 7 +- .../{ => admin}/CronjobLogGridView.class.php | 8 +- .../ExceptionLogGridView.class.php | 7 +- .../ModificationLogGridView.class.php | 6 +- .../{ => admin}/UserOptionGridView.class.php | 26 ++-- .../{ => admin}/UserRankGridView.class.php | 20 ++- .../interaction/AbstractInteraction.class.php | 43 ++++++ .../AbstractInteractionProvider.class.php | 82 +++++++++++ .../interaction/DeleteInteraction.class.php | 28 ++++ .../lib/system/interaction/Divider.class.php | 13 ++ .../interaction/EditInteraction.class.php | 21 +++ .../FormBuilderDialogInteraction.class.php | 68 +++++++++ .../system/interaction/IInteraction.class.php | 36 +++++ .../IInteractionProvider.class.php | 46 +++++++ .../InteractionConfirmationType.class.php} | 12 +- .../InteractionContextMenuView.class.php | 100 ++++++++++++++ .../LegacyDboInteraction.class.php} | 44 +++--- .../LinkInteraction.class.php} | 22 ++- .../RpcInteraction.class.php} | 44 +++--- ...daloneInteractionContextMenuView.class.php | 43 ++++++ .../interaction/ToggleInteraction.class.php | 75 ++++++++++ .../admin/UserOptionInteractions.class.php | 37 +++++ .../admin/UserRankInteractions.class.php | 37 +++++ 69 files changed, 1301 insertions(+), 536 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_interactionButton.tpl create mode 100644 com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl create mode 100644 ts/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.ts delete mode 100644 ts/WoltLabSuite/Core/Component/GridView/Action/Delete.ts rename ts/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Confirmation.ts (100%) create mode 100644 ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts rename ts/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/LegacyDboAction.ts (72%) rename ts/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Rpc.ts (66%) create mode 100644 ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts rename ts/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Toggle.ts (77%) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions.js delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Delete.js rename wcfsetup/install/files/js/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Confirmation.js (100%) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js rename wcfsetup/install/files/js/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/LegacyDboAction.js (68%) rename wcfsetup/install/files/js/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Rpc.js (59%) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js rename wcfsetup/install/files/js/WoltLabSuite/Core/Component/{GridView/Action => Interaction}/Toggle.js (80%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/ACPSessionLogGridViewInitialized.class.php (83%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/CronjobLogGridViewInitialized.class.php (83%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/ExceptionLogGridViewInitialized.class.php (83%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/ModificationLogGridViewInitialized.class.php (83%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/UserOptionGridViewInitialized.class.php (83%) rename wcfsetup/install/files/lib/event/gridView/{ => admin}/UserRankGridViewInitialized.class.php (84%) create mode 100644 wcfsetup/install/files/lib/event/interaction/admin/UserOptionInteractionCollecting.class.php create mode 100644 wcfsetup/install/files/lib/event/interaction/admin/UserRankInteractionCollecting.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/interactions/GetContextMenuOptions.class.php delete mode 100644 wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php delete mode 100644 wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php delete mode 100644 wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php delete mode 100644 wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php delete mode 100644 wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php rename wcfsetup/install/files/lib/system/gridView/{ => admin}/ACPSessionLogGridView.class.php (94%) rename wcfsetup/install/files/lib/system/gridView/{ => admin}/CronjobLogGridView.class.php (94%) rename wcfsetup/install/files/lib/system/gridView/{ => admin}/ExceptionLogGridView.class.php (95%) rename wcfsetup/install/files/lib/system/gridView/{ => admin}/ModificationLogGridView.class.php (98%) rename wcfsetup/install/files/lib/system/gridView/{ => admin}/UserOptionGridView.class.php (80%) rename wcfsetup/install/files/lib/system/gridView/{ => admin}/UserRankGridView.class.php (89%) create mode 100644 wcfsetup/install/files/lib/system/interaction/AbstractInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/AbstractInteractionProvider.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/Divider.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/EditInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/FormBuilderDialogInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/IInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/IInteractionProvider.class.php rename wcfsetup/install/files/lib/system/{gridView/action/ActionConfirmationType.class.php => interaction/InteractionConfirmationType.class.php} (60%) create mode 100644 wcfsetup/install/files/lib/system/interaction/InteractionContextMenuView.class.php rename wcfsetup/install/files/lib/system/{gridView/action/LegacyDboAction.class.php => interaction/LegacyDboInteraction.class.php} (56%) rename wcfsetup/install/files/lib/system/{gridView/action/LinkAction.class.php => interaction/LinkInteraction.class.php} (60%) rename wcfsetup/install/files/lib/system/{gridView/action/RpcAction.class.php => interaction/RpcInteraction.class.php} (55%) create mode 100644 wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuView.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/ToggleInteraction.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/admin/UserOptionInteractions.class.php create mode 100644 wcfsetup/install/files/lib/system/interaction/admin/UserRankInteractions.class.php 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/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/Confirmation.ts b/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts similarity index 100% rename from ts/WoltLabSuite/Core/Component/GridView/Action/Confirmation.ts rename to ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts 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/GridView/Action/LegacyDboAction.ts b/ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts similarity index 72% rename from ts/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.ts rename to ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts index a242bf73ca..4b04330477 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.ts @@ -13,7 +13,7 @@ import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification"; import { ConfirmationType, handleConfirmation } from "./Confirmation"; async function handleDboAction( - row: HTMLTableRowElement, + element: HTMLElement, objectName: string, className: string, actionName: string, @@ -26,14 +26,21 @@ async function handleDboAction( } await dboAction(actionName, className) - .objectIds([parseInt(row.dataset.objectId!)]) + .objectIds([parseInt(element.dataset.objectId!)]) .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {}) .dispatch(); if (confirmationType == ConfirmationType.Delete) { - row.remove(); + // 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 { - row.dispatchEvent( + element.dispatchEvent( new CustomEvent("refresh", { bubbles: true, }), @@ -44,11 +51,11 @@ async function handleDboAction( } } -export function setup(table: HTMLTableElement): void { - table.addEventListener("action", (event: CustomEvent) => { - if (event.detail.action === "legacy-dbo-action") { +export function setup(identifier: string, container: HTMLElement): void { + container.addEventListener("interaction", (event: CustomEvent) => { + if (event.detail.interaction === identifier) { void handleDboAction( - event.target as HTMLTableRowElement, + event.target as HTMLElement, event.detail.objectName, event.detail.className, event.detail.actionName, diff --git a/ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts similarity index 66% rename from ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts rename to ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts index 6c09a62bfc..9b3edd8d37 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Rpc.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts @@ -3,8 +3,8 @@ 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, +async function handleRpcInteraction( + element: HTMLElement, objectName: string, endpoint: string, confirmationType: ConfirmationType, @@ -30,10 +30,17 @@ async function handleRpcAction( } } - if (confirmationType == ConfirmationType.Delete) { - row.remove(); + 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 { - row.dispatchEvent( + element.dispatchEvent( new CustomEvent("refresh", { bubbles: true, }), @@ -44,11 +51,11 @@ async function handleRpcAction( } } -export function setup(table: HTMLTableElement): void { - table.addEventListener("action", (event: CustomEvent) => { - if (event.detail.action === "rpc") { - void handleRpcAction( - event.target as HTMLTableRowElement, +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, 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/GridView/Action/Toggle.ts b/ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts similarity index 77% rename from ts/WoltLabSuite/Core/Component/GridView/Action/Toggle.ts rename to ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts index 9b4032399f..d89d636f26 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/Action/Toggle.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/Toggle.ts @@ -5,8 +5,8 @@ async function handleToggle(checked: boolean, enableEndpoint: string, disableEnd await postObject(checked ? enableEndpoint : disableEndpoint); } -export function setup(tableId: string): void { - wheneverFirstSeen(`#${tableId} .gridView__row woltlab-core-toggle-button`, (toggleButton) => { +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, 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/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/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js similarity index 100% rename from wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Confirmation.js rename to wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js 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/GridView/Action/LegacyDboAction.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js similarity index 68% rename from wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.js rename to wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js index 1a917fe8ec..87821a3e93 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/LegacyDboAction.js @@ -11,29 +11,34 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Ui/No "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function handleDboAction(row, objectName, className, actionName, confirmationType, customConfirmationMessage = "") { + 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(row.dataset.objectId)]) + .objectIds([parseInt(element.dataset.objectId)]) .payload(confirmationResult.reason ? { reason: confirmationResult.reason } : {}) .dispatch(); if (confirmationType == Confirmation_1.ConfirmationType.Delete) { - row.remove(); + // 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 { - row.dispatchEvent(new CustomEvent("refresh", { + 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(table) { - table.addEventListener("action", (event) => { - if (event.detail.action === "legacy-dbo-action") { + 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/GridView/Action/Rpc.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js similarity index 59% rename from wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Rpc.js rename to wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js index 4cf316af53..458293bc29 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Rpc.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js @@ -2,7 +2,7 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function handleRpcAction(row, objectName, endpoint, confirmationType, customConfirmationMessage = "") { + async function handleRpcInteraction(element, objectName, endpoint, confirmationType, customConfirmationMessage = "") { const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage); if (!confirmationResult.result) { return; @@ -19,21 +19,26 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit return; } } - if (confirmationType == Confirmation_1.ConfirmationType.Delete) { - row.remove(); + 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 { - row.dispatchEvent(new CustomEvent("refresh", { + 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(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); + 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/GridView/Action/Toggle.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js similarity index 80% rename from wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Toggle.js rename to wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js index af05f9c295..191b7bd8e7 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Action/Toggle.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Toggle.js @@ -5,8 +5,8 @@ define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite 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) => { + 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/admin/ACPSessionLogGridViewInitialized.class.php similarity index 83% rename from wcfsetup/install/files/lib/event/gridView/ACPSessionLogGridViewInitialized.class.php rename to wcfsetup/install/files/lib/event/gridView/admin/ACPSessionLogGridViewInitialized.class.php index 43f1430006..61d2f9a2f7 100644 --- a/wcfsetup/install/files/lib/event/gridView/ACPSessionLogGridViewInitialized.class.php +++ b/wcfsetup/install/files/lib/event/gridView/admin/ACPSessionLogGridViewInitialized.class.php @@ -1,9 +1,9 @@ + * @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/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/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/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/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/ACPSessionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php similarity index 94% rename from wcfsetup/install/files/lib/system/gridView/ACPSessionLogGridView.class.php rename to wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php index 88c45dde84..48eadf68c6 100644 --- a/wcfsetup/install/files/lib/system/gridView/ACPSessionLogGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/ACPSessionLogGridView.class.php @@ -1,17 +1,20 @@ {$label} - + + 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/gridView/action/ActionConfirmationType.class.php b/wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php similarity index 60% rename from wcfsetup/install/files/lib/system/gridView/action/ActionConfirmationType.class.php rename to wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php index 7bc5e9998c..30854d42ed 100644 --- a/wcfsetup/install/files/lib/system/gridView/action/ActionConfirmationType.class.php +++ b/wcfsetup/install/files/lib/system/interaction/InteractionConfirmationType.class.php @@ -1,8 +1,16 @@ + * @since 6.2 + */ +enum InteractionConfirmationType { case None; case SoftDelete; 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/gridView/action/LegacyDboAction.class.php b/wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php similarity index 56% rename from wcfsetup/install/files/lib/system/gridView/action/LegacyDboAction.class.php rename to wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php index 288f155aa7..ca956dc07a 100644 --- a/wcfsetup/install/files/lib/system/gridView/action/LegacyDboAction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/LegacyDboInteraction.class.php @@ -1,16 +1,16 @@ getIdentifier()); if (\is_string($this->languageItem)) { $label = WCF::getLanguage()->get($this->languageItem); } else { - $label = ($this->languageItem)($row); + $label = ($this->languageItem)($object); } if (\is_string($this->confirmationMessage)) { $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage); } else { - $confirmationMessage = ($this->confirmationMessage)($row); + $confirmationMessage = ($this->confirmationMessage)($object); } - if ($row instanceof ITitledObject) { - $objectName = StringUtil::encodeHTML($row->getTitle()); + if ($object instanceof ITitledObject) { + $objectName = StringUtil::encodeHTML($object->getTitle()); } else { $objectName = ''; } @@ -60,7 +61,7 @@ class LegacyDboAction extends AbstractAction return <<getID()); + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); return << - require(['WoltLabSuite/Core/Component/GridView/Action/LegacyDboAction'], ({ setup }) => { - setup(document.getElementById('{$id}_table')); + require(['WoltLabSuite/Core/Component/Interaction/LegacyDboAction'], ({ setup }) => { + setup('{$identifier}', document.getElementById('{$containerId}')); }); HTML; diff --git a/wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php b/wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php similarity index 60% rename from wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php rename to wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php index 2173117e1f..ff32cc6585 100644 --- a/wcfsetup/install/files/lib/system/gridView/action/LinkAction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/LinkInteraction.class.php @@ -1,45 +1,43 @@ * @since 6.2 */ -class LinkAction extends AbstractAction +class LinkInteraction extends AbstractInteraction { public function __construct( + string $identifier, protected readonly string $controllerClass, - protected readonly string|Closure $languageItem, - ?Closure $isAvailableCallback = null + protected readonly string|\Closure $languageItem, + ?\Closure $isAvailableCallback = null ) { - parent::__construct($isAvailableCallback); + parent::__construct($identifier, $isAvailableCallback); } #[\Override] - public function render(mixed $row): string + public function render(DatabaseObject $object): string { - \assert($row instanceof DatabaseObject); $href = LinkHandler::getInstance()->getControllerLink( $this->controllerClass, - ['object' => $row] + ['object' => $object] ); if (\is_string($this->languageItem)) { $title = WCF::getLanguage()->get($this->languageItem); } else { - $title = ($this->languageItem)($row); + $title = ($this->languageItem)($object); } 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/interaction/RpcInteraction.class.php similarity index 55% rename from wcfsetup/install/files/lib/system/gridView/action/RpcAction.class.php rename to wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php index ba02b0a336..d9ae46dc46 100644 --- a/wcfsetup/install/files/lib/system/gridView/action/RpcAction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php @@ -1,60 +1,59 @@ * @since 6.2 */ -class RpcAction extends AbstractAction +class RpcInteraction extends AbstractInteraction { public function __construct( + string $identifier, protected readonly string $endpoint, - protected readonly string|Closure $languageItem, - protected readonly ActionConfirmationType $confirmationType = ActionConfirmationType::None, - protected readonly string|Closure $confirmationMessage = '', - ?Closure $isAvailableCallback = null + protected readonly string|\Closure $languageItem, + protected readonly InteractionConfirmationType $confirmationType = InteractionConfirmationType::None, + protected readonly string|\Closure $confirmationMessage = '', + ?\Closure $isAvailableCallback = null ) { - parent::__construct($isAvailableCallback); + parent::__construct($identifier, $isAvailableCallback); } #[\Override] - public function render(mixed $row): string + public function render(DatabaseObject $object): string { - \assert($row instanceof DatabaseObject); + $identifier = StringUtil::encodeJS($this->getIdentifier()); if (\is_string($this->languageItem)) { $label = WCF::getLanguage()->get($this->languageItem); } else { - $label = ($this->languageItem)($row); + $label = ($this->languageItem)($object); } if (\is_string($this->confirmationMessage)) { $confirmationMessage = WCF::getLanguage()->get($this->confirmationMessage); } else { - $confirmationMessage = ($this->confirmationMessage)($row); + $confirmationMessage = ($this->confirmationMessage)($object); } $endpoint = StringUtil::encodeHTML( LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . - \sprintf($this->endpoint, $row->getObjectID()) + \sprintf($this->endpoint, $object->getObjectID()) ); - if ($row instanceof ITitledObject) { - $objectName = StringUtil::encodeHTML($row->getTitle()); + if ($object instanceof ITitledObject) { + $objectName = StringUtil::encodeHTML($object->getTitle()); } else { $objectName = ''; } @@ -62,7 +61,7 @@ class RpcAction extends AbstractAction return <<getID()); + $identifier = StringUtil::encodeJS($this->getIdentifier()); + $containerId = StringUtil::encodeJS($containerId); return << - require(['WoltLabSuite/Core/Component/GridView/Action/Rpc'], ({ setup }) => { - setup(document.getElementById('{$id}_table')); + 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; + } +} -- 2.20.1