From e25ceadb997ffdec94638291f43fcccd27466eff Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 1 Apr 2025 11:01:13 +0200 Subject: [PATCH 1/2] Implement sorting for attachments --- .../Core/Component/Attachment/List.ts | 53 +++++++++- .../Core/Component/Attachment/List.js | 44 +++++++- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../attachments/ChangeShowOrder.class.php | 100 ++++++++++++++++++ wcfsetup/install/files/style/ui/fileList.scss | 20 +++- 5 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php diff --git a/ts/WoltLabSuite/Core/Component/Attachment/List.ts b/ts/WoltLabSuite/Core/Component/Attachment/List.ts index aa6c7e510c1..5b31e45c31e 100644 --- a/ts/WoltLabSuite/Core/Component/Attachment/List.ts +++ b/ts/WoltLabSuite/Core/Component/Attachment/List.ts @@ -3,9 +3,30 @@ import { CkeditorDropEvent } from "../File/Upload"; import { createAttachmentFromFile } from "./Entry"; import { listenToCkeditor } from "../Ckeditor/Event"; import { getTabMenu } from "../Message/MessageTabMenu"; +import Sortable from "sortablejs"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; +import { postObject } from "WoltLabSuite/Core/Api/PostObject"; +import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; + +async function addSortableHandler(container: HTMLLIElement, file: WoltlabCoreFileElement) { + await file.ready; + + container.dataset.attachmentId = (file.data!.attachmentID as number).toString(); + + const icon = document.createElement("fa-icon"); + icon.setIcon("up-down-left-right"); + const handle = document.createElement("span"); + handle.append(icon); + handle.classList.add("sortableList__handle"); + container.prepend(handle); +} function fileToAttachment(fileList: HTMLElement, file: WoltlabCoreFileElement, editor: HTMLElement): void { - fileList.append(createAttachmentFromFile(file, editor)); + const container = createAttachmentFromFile(file, editor); + + void addSortableHandler(container, file); + + fileList.append(container); } type Context = { @@ -44,6 +65,36 @@ export function setup(editorId: string): void { uploadButton.insertAdjacentElement("afterend", fileList); } + const sortable = new Sortable(fileList, { + direction: "vertical", + dataIdAttr: "data-attachment-id", + dragClass: ".fileList__item", + handle: ".sortableList__handle", + animation: 150, + fallbackOnBody: true, + onChange: (event) => { + const file = event.item.querySelector("woltlab-core-file")!; + const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); + if (thumbnail !== undefined) { + file.thumbnail = thumbnail; + } else if (file.link) { + file.previewUrl = file.link; + } + }, + onEnd: promiseMutex(async (event) => { + if (event.oldIndex === event.newIndex) { + return; + } + + const attachmentIDs = sortable.toArray().map(Number); + const context = JSON.parse(uploadButton.dataset.context!); + + await postObject(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); + + showDefaultSuccessSnackbar(); + }), + }); + let showOrder = -1; uploadButton.addEventListener("uploadStart", (event: CustomEvent) => { fileToAttachment(fileList, event.detail, editor); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js index 41da2da7fe6..e477839ba67 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js @@ -1,9 +1,22 @@ -define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu"], function (require, exports, Entry_1, Event_1, MessageTabMenu_1) { +define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu", "sortablejs", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Api/PostObject", "WoltLabSuite/Core/Component/Snackbar"], function (require, exports, tslib_1, Entry_1, Event_1, MessageTabMenu_1, sortablejs_1, PromiseMutex_1, PostObject_1, Snackbar_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; + sortablejs_1 = tslib_1.__importDefault(sortablejs_1); + async function addSortableHandler(container, file) { + await file.ready; + container.dataset.attachmentId = file.data.attachmentID.toString(); + const icon = document.createElement("fa-icon"); + icon.setIcon("up-down-left-right"); + const handle = document.createElement("span"); + handle.append(icon); + handle.classList.add("sortableList__handle"); + container.prepend(handle); + } function fileToAttachment(fileList, file, editor) { - fileList.append((0, Entry_1.createAttachmentFromFile)(file, editor)); + const container = (0, Entry_1.createAttachmentFromFile)(file, editor); + void addSortableHandler(container, file); + fileList.append(container); } function setup(editorId) { const container = document.getElementById(`attachments_${editorId}`); @@ -32,6 +45,33 @@ define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/Messag fileList.classList.add("fileList"); uploadButton.insertAdjacentElement("afterend", fileList); } + const sortable = new sortablejs_1.default(fileList, { + direction: "vertical", + dataIdAttr: "data-attachment-id", + dragClass: ".fileList__item", + handle: ".sortableList__handle", + animation: 150, + fallbackOnBody: true, + onChange: (event) => { + const file = event.item.querySelector("woltlab-core-file"); + const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); + if (thumbnail !== undefined) { + file.thumbnail = thumbnail; + } + else if (file.link) { + file.previewUrl = file.link; + } + }, + onEnd: (0, PromiseMutex_1.promiseMutex)(async (event) => { + if (event.oldIndex === event.newIndex) { + return; + } + const attachmentIDs = sortable.toArray().map(Number); + const context = JSON.parse(uploadButton.dataset.context); + await (0, PostObject_1.postObject)(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); + (0, Snackbar_1.showDefaultSuccessSnackbar)(); + }), + }); let showOrder = -1; uploadButton.addEventListener("uploadStart", (event) => { fileToAttachment(fileList, event.detail, editor); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 69e00d6e778..4f233f8e7c5 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -238,6 +238,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\smilies\ChangeShowOrder()); $event->register(new \wcf\system\endpoint\controller\core\smilies\categories\GetSmileyShowOrder()); $event->register(new \wcf\system\endpoint\controller\core\smilies\categories\ChangeSmileyShowOrder()); + $event->register(new \wcf\system\endpoint\controller\core\attachments\ChangeShowOrder()); } ); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php new file mode 100644 index 00000000000..f987e535b31 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php @@ -0,0 +1,100 @@ + + * @since 6.2 + */ +#[PostRequest('/core/attachments/show-order')] +final class ChangeShowOrder implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $parameters = Helper::mapApiParameters($request, ChangeShowOrderParameters::class); + + $attachmentHandler = new AttachmentHandler( + $parameters->objectType, + $parameters->objectID, + $parameters->tmpHash, + $parameters->parentObjectID, + ); + + $this->assertAttachmentsCanBeSorted($attachmentHandler, $parameters->attachmentIDs); + + $this->saveShowOrder($parameters->attachmentIDs); + + return new JsonResponse([]); + } + + /** + * @param list $attachmentIDs + */ + private function assertAttachmentsCanBeSorted(AttachmentHandler $attachmentHandler, array $attachmentIDs): void + { + if (!$attachmentHandler->canUpload()) { + throw new PermissionDeniedException(); + } + + $attachmentList = $attachmentHandler->getAttachmentList(); + foreach ($attachmentIDs as $attachmentID) { + if (!\in_array($attachmentID, $attachmentList->getObjectIDs(), true)) { + throw new PermissionDeniedException(); + } + } + } + + /** + * @param list $attachmentIDs + */ + private function saveShowOrder(array $attachmentIDs): void + { + WCF::getDB()->beginTransaction(); + + $sql = "UPDATE wcf1_attachment + SET showOrder = ? + WHERE attachmentID = ?"; + $statement = WCF::getDB()->prepare($sql); + + foreach ($attachmentIDs as $showOrder => $attachmentID) { + $statement->execute([ + $showOrder + 1, + $attachmentID, + ]); + } + + WCF::getDB()->commitTransaction(); + } +} + +/** @internal */ +final class ChangeShowOrderParameters +{ + public function __construct( + /** @var non-empty-string */ + public readonly string $objectType, + /** @var non-negative-int */ + public readonly int $objectID, + /** @var non-negative-int */ + public readonly int $parentObjectID, + public readonly string $tmpHash, + /** @var list */ + public readonly array $attachmentIDs, + ) { + } +} diff --git a/wcfsetup/install/files/style/ui/fileList.scss b/wcfsetup/install/files/style/ui/fileList.scss index ccd0a1067f2..55582d43661 100644 --- a/wcfsetup/install/files/style/ui/fileList.scss +++ b/wcfsetup/install/files/style/ui/fileList.scss @@ -23,11 +23,11 @@ box-shadow: var(--wcfBoxShadowCard); display: grid; grid-template-areas: - "file filename" - "file fileSize" - "file buttons" - "file error"; - grid-template-columns: 80px auto; + "file filename sortableHandle" + "file fileSize sortableHandle" + "file buttons buttons" + "file error error"; + grid-template-columns: 80px auto 20px; padding: 10px; } @@ -106,3 +106,13 @@ woltlab-core-file img { .woltlabCoreFileUpload__input::-webkit-file-upload-button { cursor: pointer; } + +.fileList__item .sortableList__handle { + grid-area: sortableHandle; +} + +@media (hover: hover) { + .fileList__item .sortableList__handle:hover { + cursor: move; + } +} From 966c37849e0c12192b7bdfa5c596dfecea84ddfb Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 4 Apr 2025 13:28:14 +0200 Subject: [PATCH 2/2] Simplify and fix the attachment sorting --- .../Core/Component/Attachment/List.ts | 34 +++++-------------- .../Core/Component/Attachment/List.js | 29 +++++----------- wcfsetup/install/files/style/ui/fileList.scss | 20 +++++------ 3 files changed, 27 insertions(+), 56 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Attachment/List.ts b/ts/WoltLabSuite/Core/Component/Attachment/List.ts index 5b31e45c31e..e885384fd6f 100644 --- a/ts/WoltLabSuite/Core/Component/Attachment/List.ts +++ b/ts/WoltLabSuite/Core/Component/Attachment/List.ts @@ -6,27 +6,9 @@ import { getTabMenu } from "../Message/MessageTabMenu"; import Sortable from "sortablejs"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { postObject } from "WoltLabSuite/Core/Api/PostObject"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -async function addSortableHandler(container: HTMLLIElement, file: WoltlabCoreFileElement) { - await file.ready; - - container.dataset.attachmentId = (file.data!.attachmentID as number).toString(); - - const icon = document.createElement("fa-icon"); - icon.setIcon("up-down-left-right"); - const handle = document.createElement("span"); - handle.append(icon); - handle.classList.add("sortableList__handle"); - container.prepend(handle); -} function fileToAttachment(fileList: HTMLElement, file: WoltlabCoreFileElement, editor: HTMLElement): void { - const container = createAttachmentFromFile(file, editor); - - void addSortableHandler(container, file); - - fileList.append(container); + fileList.append(createAttachmentFromFile(file, editor)); } type Context = { @@ -65,14 +47,14 @@ export function setup(editorId: string): void { uploadButton.insertAdjacentElement("afterend", fileList); } - const sortable = new Sortable(fileList, { + new Sortable(fileList, { direction: "vertical", - dataIdAttr: "data-attachment-id", dragClass: ".fileList__item", - handle: ".sortableList__handle", + ghostClass: "fileList__item--ghost", + handle: ".fileList__item__file", animation: 150, fallbackOnBody: true, - onChange: (event) => { + onChange(event) { const file = event.item.querySelector("woltlab-core-file")!; const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); if (thumbnail !== undefined) { @@ -86,12 +68,12 @@ export function setup(editorId: string): void { return; } - const attachmentIDs = sortable.toArray().map(Number); + const attachmentIDs = Array.from(fileList.querySelectorAll("woltlab-core-file")) + .map((file) => file.data?.attachmentID) + .filter((attachmentID) => attachmentID !== undefined); const context = JSON.parse(uploadButton.dataset.context!); await postObject(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); - - showDefaultSuccessSnackbar(); }), }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js index e477839ba67..2c89d0369b0 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js @@ -1,22 +1,10 @@ -define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu", "sortablejs", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Api/PostObject", "WoltLabSuite/Core/Component/Snackbar"], function (require, exports, tslib_1, Entry_1, Event_1, MessageTabMenu_1, sortablejs_1, PromiseMutex_1, PostObject_1, Snackbar_1) { +define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu", "sortablejs", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Api/PostObject"], function (require, exports, tslib_1, Entry_1, Event_1, MessageTabMenu_1, sortablejs_1, PromiseMutex_1, PostObject_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; sortablejs_1 = tslib_1.__importDefault(sortablejs_1); - async function addSortableHandler(container, file) { - await file.ready; - container.dataset.attachmentId = file.data.attachmentID.toString(); - const icon = document.createElement("fa-icon"); - icon.setIcon("up-down-left-right"); - const handle = document.createElement("span"); - handle.append(icon); - handle.classList.add("sortableList__handle"); - container.prepend(handle); - } function fileToAttachment(fileList, file, editor) { - const container = (0, Entry_1.createAttachmentFromFile)(file, editor); - void addSortableHandler(container, file); - fileList.append(container); + fileList.append((0, Entry_1.createAttachmentFromFile)(file, editor)); } function setup(editorId) { const container = document.getElementById(`attachments_${editorId}`); @@ -45,14 +33,14 @@ define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Messa fileList.classList.add("fileList"); uploadButton.insertAdjacentElement("afterend", fileList); } - const sortable = new sortablejs_1.default(fileList, { + new sortablejs_1.default(fileList, { direction: "vertical", - dataIdAttr: "data-attachment-id", dragClass: ".fileList__item", - handle: ".sortableList__handle", + ghostClass: "fileList__item--ghost", + handle: ".fileList__item__file", animation: 150, fallbackOnBody: true, - onChange: (event) => { + onChange(event) { const file = event.item.querySelector("woltlab-core-file"); const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); if (thumbnail !== undefined) { @@ -66,10 +54,11 @@ define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Messa if (event.oldIndex === event.newIndex) { return; } - const attachmentIDs = sortable.toArray().map(Number); + const attachmentIDs = Array.from(fileList.querySelectorAll("woltlab-core-file")) + .map((file) => file.data?.attachmentID) + .filter((attachmentID) => attachmentID !== undefined); const context = JSON.parse(uploadButton.dataset.context); await (0, PostObject_1.postObject)(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); }), }); let showOrder = -1; diff --git a/wcfsetup/install/files/style/ui/fileList.scss b/wcfsetup/install/files/style/ui/fileList.scss index 55582d43661..ab177a4e076 100644 --- a/wcfsetup/install/files/style/ui/fileList.scss +++ b/wcfsetup/install/files/style/ui/fileList.scss @@ -23,11 +23,11 @@ box-shadow: var(--wcfBoxShadowCard); display: grid; grid-template-areas: - "file filename sortableHandle" - "file fileSize sortableHandle" - "file buttons buttons" - "file error error"; - grid-template-columns: 80px auto 20px; + "file filename" + "file fileSize" + "file buttons" + "file error"; + grid-template-columns: 80px auto; padding: 10px; } @@ -39,6 +39,10 @@ color: var(--wcfStatusErrorText); } +.fileList__item--ghost { + opacity: 0.54; +} + .fileList__item .innerError { grid-area: error; } @@ -107,12 +111,8 @@ woltlab-core-file img { cursor: pointer; } -.fileList__item .sortableList__handle { - grid-area: sortableHandle; -} - @media (hover: hover) { - .fileList__item .sortableList__handle:hover { + .fileList__item__file:hover { cursor: move; } }