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..97ab2dc7558 100644 --- a/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl +++ b/wcfsetup/install/files/acp/templates/labelGroupAdd.tpl @@ -23,6 +23,7 @@