From abf5a7ea3ea2003a254304af3b622bec5ae788d9 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sat, 15 Mar 2025 13:19:36 +0100 Subject: [PATCH 1/3] Generic implementation for the editing of show orders via drag and drop --- ts/WoltLabSuite/Core/Api/GetObject.ts | 24 ++++++ .../Core/Component/ChangeShowOrder.ts | 82 +++++++++++++++++++ .../files/acp/templates/labelGroupAdd.tpl | 12 +++ .../files/acp/templates/labelGroupList.tpl | 16 ++++ .../js/WoltLabSuite/Core/Api/GetObject.js | 24 ++++++ .../Core/Component/ChangeShowOrder.js | 60 ++++++++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 4 + .../groups/ChangeLabelShowOrder.class.php | 64 +++++++++++++++ .../labels/groups/ChangeShowOrder.class.php | 62 ++++++++++++++ .../labels/groups/GetLabelShowOrder.class.php | 40 +++++++++ .../core/labels/groups/GetShowOrder.class.php | 37 +++++++++ .../showOrder/ShowOrderHandler.class.php | 62 ++++++++++++++ .../system/showOrder/ShowOrderItem.class.php | 19 +++++ wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 15 files changed, 508 insertions(+) create mode 100644 ts/WoltLabSuite/Core/Api/GetObject.ts create mode 100644 ts/WoltLabSuite/Core/Component/ChangeShowOrder.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/GetObject.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/ChangeShowOrder.js create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/labels/groups/ChangeLabelShowOrder.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/labels/groups/ChangeShowOrder.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/labels/groups/GetLabelShowOrder.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/labels/groups/GetShowOrder.class.php create mode 100644 wcfsetup/install/files/lib/system/showOrder/ShowOrderHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/showOrder/ShowOrderItem.class.php diff --git a/ts/WoltLabSuite/Core/Api/GetObject.ts b/ts/WoltLabSuite/Core/Api/GetObject.ts new file mode 100644 index 00000000000..cb5cd1afccc --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/GetObject.ts @@ -0,0 +1,24 @@ +/** + * Sends a get request to the given endpoint. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "./Result"; + +export async function getObject(endpoint: string): Promise> { + let response: T; + + try { + response = (await prepareRequest(endpoint).get().fetchAsJson()) as T; + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue(response); +} diff --git a/ts/WoltLabSuite/Core/Component/ChangeShowOrder.ts b/ts/WoltLabSuite/Core/Component/ChangeShowOrder.ts new file mode 100644 index 00000000000..98aaa607d30 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/ChangeShowOrder.ts @@ -0,0 +1,82 @@ +/** + * Handles the change of the show order of elements. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ + +import { getObject } from "../Api/GetObject"; +import { postObject } from "../Api/PostObject"; +import { promiseMutex } from "../Helper/PromiseMutex"; +import { getPhrase } from "../Language"; +import { dialogFactory } from "./Dialog"; +import Sortable from "sortablejs"; +import { showDefaultSuccessSnackbar } from "./Snackbar"; + +type Item = { + id: number; + label: string; +}; + +async function showDialog(endpoint: string): Promise { + const items = await getItems(endpoint); + + const dialog = dialogFactory().fromHtml(getHtml(items)).asPrompt(); + dialog.show(getPhrase("wcf.global.changeShowOrder")); + + const sortable = new Sortable(dialog.content.querySelector(".sortableList")!, { + direction: "vertical", + animation: 150, + fallbackOnBody: true, + dataIdAttr: "data-object-id", + draggable: "li", + handle: ".sortableList__handle", + }); + + dialog.addEventListener("primary", () => { + void saveItems(endpoint, sortable.toArray().map(Number)).then(() => { + showDefaultSuccessSnackbar().addEventListener("snackbar:close", () => { + window.location.reload(); + }); + }); + }); +} + +async function getItems(endpoint: string): Promise { + return (await getObject(`${window.WSC_RPC_API_URL}${endpoint}`)).unwrap(); +} + +async function saveItems(endpoint: string, values: number[]): Promise { + await postObject(`${window.WSC_RPC_API_URL}${endpoint}`, { values }); +} + +function getHtml(items: Item[]): string { + const list = document.createElement("ol"); + list.classList.add("sortableList"); + + items.forEach((item) => { + const listItem = document.createElement("li"); + listItem.dataset.objectId = item.id.toString(); + listItem.textContent = item.label; + + const icon = document.createElement("fa-icon"); + icon.setIcon("up-down"); + const handle = document.createElement("span"); + handle.append(icon); + handle.classList.add("sortableList__handle"); + listItem.prepend(handle); + + list.append(listItem); + }); + + return list.outerHTML; +} + +export function setup(button: HTMLElement, endpoint: string): void { + button.addEventListener( + "click", + promiseMutex(() => showDialog(endpoint)), + ); +} diff --git a/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl b/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl index d8569d3c6cf..7baef14906e 100644 --- a/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl +++ b/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl @@ -23,6 +23,7 @@