From: Alexander Ebert Date: Fri, 3 May 2024 13:02:47 +0000 (+0200) Subject: Improve the UI of uploaded attachments X-Git-Tag: 6.1.0_Alpha_1~85^2^2~35 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=3fd7cc2fc6ccbbb1d0e99d0abbb82a54decd3286;p=GitHub%2FWoltLab%2FWCF.git Improve the UI of uploaded attachments --- diff --git a/ts/WoltLabSuite/Core/Api/Error.ts b/ts/WoltLabSuite/Core/Api/Error.ts index 7ea2b4e81d..663bdf7e0a 100644 --- a/ts/WoltLabSuite/Core/Api/Error.ts +++ b/ts/WoltLabSuite/Core/Api/Error.ts @@ -27,7 +27,7 @@ export class ApiError { } } -class ValidationError { +export class ValidationError { constructor( public readonly code: string, public readonly message: string, diff --git a/ts/WoltLabSuite/Core/Component/Attachment/List.ts b/ts/WoltLabSuite/Core/Component/Attachment/List.ts index 9ec565cd1c..3ad3bb3084 100644 --- a/ts/WoltLabSuite/Core/Component/Attachment/List.ts +++ b/ts/WoltLabSuite/Core/Component/Attachment/List.ts @@ -4,6 +4,7 @@ import WoltlabCoreFileElement from "../File/woltlab-core-file"; import "../File/woltlab-core-file"; import { CkeditorDropEvent } from "../File/Upload"; +import { formatFilesize } from "WoltLabSuite/Core/FileUtil"; type FileProcessorData = { attachmentID: number; @@ -19,49 +20,63 @@ function upload(fileList: HTMLElement, file: WoltlabCoreFileElement, editorId: s const filename = document.createElement("div"); filename.classList.add("attachment__item__filename"); - filename.textContent = file.filename!; + filename.textContent = file.filename || file.dataset.filename!; - element.append(fileWrapper, filename); + const fileSize = document.createElement("div"); + fileSize.classList.add("attachment__item__fileSize"); + fileSize.textContent = formatFilesize(file.fileSize || parseInt(file.dataset.fileSize!)); + + element.append(fileWrapper, filename, fileSize); fileList.append(element); - void file.ready.then(() => { - const data = file.data; - if (data === undefined) { - // TODO: error handling - return; - } + void file.ready + .then(() => { + const data = file.data; + if (data === undefined) { + // TODO: error handling + return; + } - const fileId = file.fileId; - if (fileId === undefined) { - // TODO: error handling - return; - } + const fileId = file.fileId; + if (fileId === undefined) { + // TODO: error handling + return; + } - const buttonList = document.createElement("div"); - buttonList.classList.add("attachment__item__buttons"); - buttonList.append( - getDeleteAttachButton(fileId, (data as FileProcessorData).attachmentID, editorId, element), - getInsertAttachBbcodeButton( - (data as FileProcessorData).attachmentID, - file.isImage() && file.link ? file.link : "", - editorId, - ), - ); - - if (file.isImage()) { - const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); - if (thumbnail !== undefined) { - file.thumbnail = thumbnail; + const buttonList = document.createElement("div"); + buttonList.classList.add("attachment__item__buttons"); + buttonList.append( + getDeleteAttachButton(fileId, (data as FileProcessorData).attachmentID, editorId, element), + getInsertAttachBbcodeButton( + (data as FileProcessorData).attachmentID, + file.isImage() && file.link ? file.link : "", + editorId, + ), + ); + + if (file.isImage()) { + const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); + if (thumbnail !== undefined) { + file.thumbnail = thumbnail; + } + + const url = file.thumbnails.find((thumbnail) => thumbnail.identifier === "")?.link; + if (url !== undefined) { + buttonList.append(getInsertThumbnailButton((data as FileProcessorData).attachmentID, url, editorId)); + } } - const url = file.thumbnails.find((thumbnail) => thumbnail.identifier === "")?.link; - if (url !== undefined) { - buttonList.append(getInsertThumbnailButton((data as FileProcessorData).attachmentID, url, editorId)); + element.append(buttonList); + }) + .catch(() => { + if (file.validationError === undefined) { + return; } - } - element.append(buttonList); - }); + // TODO: Add a proper error message, this is for development purposes only. + element.append(JSON.stringify(file.validationError)); + element.classList.add("attachment__item--error"); + }); } function getDeleteAttachButton( diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index d90246a556..0347930376 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -41,6 +41,7 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis const fileElement = document.createElement("woltlab-core-file"); fileElement.dataset.filename = file.name; + fileElement.dataset.fileSize = file.size.toString(); const event = new CustomEvent("uploadStart", { detail: fileElement }); element.dispatchEvent(event); @@ -49,12 +50,12 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis if (!response.ok) { const validationError = response.error.getValidationError(); if (validationError === undefined) { - fileElement.uploadFailed(); + fileElement.uploadFailed(undefined); throw response.error; } - console.log(validationError); + fileElement.uploadFailed(validationError); return undefined; } @@ -73,7 +74,7 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis const response = await uploadChunk(identifier, i, checksum, chunk); if (!response.ok) { - fileElement.uploadFailed(); + fileElement.uploadFailed(undefined); throw response.error; } diff --git a/ts/WoltLabSuite/Core/Component/File/woltlab-core-file.ts b/ts/WoltLabSuite/Core/Component/File/woltlab-core-file.ts index 200893b4b6..c8dbeb48e1 100644 --- a/ts/WoltLabSuite/Core/Component/File/woltlab-core-file.ts +++ b/ts/WoltLabSuite/Core/Component/File/woltlab-core-file.ts @@ -1,3 +1,4 @@ +import type { ValidationError } from "WoltLabSuite/Core/Api/Error"; import { getExtensionByMimeType, getIconNameByFilename } from "WoltLabSuite/Core/FileUtil"; const enum State { @@ -35,9 +36,11 @@ export class WoltlabCoreFileElement extends HTMLElement { #data: Record | undefined = undefined; #filename: string = ""; #fileId: number | undefined = undefined; + #fileSize: number | undefined = undefined; #link: string | undefined = undefined; #mimeType: string | undefined = undefined; #state: State = State.Initial; + #validationError: ValidationError | undefined = undefined; readonly #thumbnails: Thumbnail[] = []; #readyReject!: () => void; @@ -70,9 +73,12 @@ export class WoltlabCoreFileElement extends HTMLElement { // Files that exist at page load have a valid file id, otherwise a new // file element can only be the result of an upload attempt. if (this.#fileId === undefined) { - this.#filename = this.dataset.filename || "bogus.bin"; + this.#filename = this.dataset.filename || "unknown.bin"; delete this.dataset.filename; + this.#fileSize = parseInt(this.dataset.fileSize || "0"); + delete this.dataset.fileSize; + this.#mimeType = this.dataset.mimeType || "application/octet-stream"; delete this.dataset.mimeType; @@ -129,7 +135,7 @@ export class WoltlabCoreFileElement extends HTMLElement { break; case State.Failed: - this.#replaceWithIcon("times"); + this.#replaceWithIcon("triangle-exclamation"); break; default: @@ -223,6 +229,10 @@ export class WoltlabCoreFileElement extends HTMLElement { return this.#filename; } + get fileSize(): number | undefined { + return this.#fileSize; + } + get mimeType(): string | undefined { return this.#mimeType; } @@ -252,12 +262,13 @@ export class WoltlabCoreFileElement extends HTMLElement { } } - uploadFailed(): void { + uploadFailed(validationError: ValidationError | undefined): void { if (this.#state !== State.Uploading) { return; } this.#state = State.Failed; + this.#validationError = validationError; this.#rebuildElement(); this.#readyReject(); @@ -319,6 +330,10 @@ export class WoltlabCoreFileElement extends HTMLElement { get ready(): Promise { return this.#readyPromise; } + + get validationError(): ValidationError | undefined { + return this.#validationError; + } } export default WoltlabCoreFileElement; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js index 640e2de721..9eddeae174 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js @@ -9,7 +9,7 @@ define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.ApiError = void 0; + exports.ValidationError = exports.ApiError = void 0; class ApiError { type; code; @@ -41,4 +41,5 @@ define(["require", "exports"], function (require, exports) { this.param = param; } } + exports.ValidationError = ValidationError; }); 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 e6c59bf815..4c34100553 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js @@ -1,4 +1,4 @@ -define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Ckeditor/Event", "../File/woltlab-core-file"], function (require, exports, DeleteFile_1, Event_1) { +define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Ckeditor/Event", "WoltLabSuite/Core/FileUtil", "../File/woltlab-core-file"], function (require, exports, DeleteFile_1, Event_1, FileUtil_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = void 0; @@ -10,10 +10,14 @@ define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Cked fileWrapper.append(file); const filename = document.createElement("div"); filename.classList.add("attachment__item__filename"); - filename.textContent = file.filename; - element.append(fileWrapper, filename); + filename.textContent = file.filename || file.dataset.filename; + const fileSize = document.createElement("div"); + fileSize.classList.add("attachment__item__fileSize"); + fileSize.textContent = (0, FileUtil_1.formatFilesize)(file.fileSize || parseInt(file.dataset.fileSize)); + element.append(fileWrapper, filename, fileSize); fileList.append(element); - void file.ready.then(() => { + void file.ready + .then(() => { const data = file.data; if (data === undefined) { // TODO: error handling @@ -38,6 +42,14 @@ define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Cked } } element.append(buttonList); + }) + .catch(() => { + if (file.validationError === undefined) { + return; + } + // TODO: Add a proper error message, this is for development purposes only. + element.append(JSON.stringify(file.validationError)); + element.classList.add("attachment__item--error"); }); } function getDeleteAttachButton(fileId, attachmentId, editorId, element) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index d3baeb81a6..2c1395fd68 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -8,16 +8,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol const fileHash = await getSha256Hash(await file.arrayBuffer()); const fileElement = document.createElement("woltlab-core-file"); fileElement.dataset.filename = file.name; + fileElement.dataset.fileSize = file.size.toString(); const event = new CustomEvent("uploadStart", { detail: fileElement }); element.dispatchEvent(event); const response = await (0, Upload_1.upload)(file.name, file.size, fileHash, typeName, element.dataset.context || ""); if (!response.ok) { const validationError = response.error.getValidationError(); if (validationError === undefined) { - fileElement.uploadFailed(); + fileElement.uploadFailed(undefined); throw response.error; } - console.log(validationError); + fileElement.uploadFailed(validationError); return undefined; } const { identifier, numberOfChunks } = response.value; @@ -30,7 +31,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol const checksum = await getSha256Hash(await chunk.arrayBuffer()); const response = await (0, Chunk_1.uploadChunk)(identifier, i, checksum, chunk); if (!response.ok) { - fileElement.uploadFailed(); + fileElement.uploadFailed(undefined); throw response.error; } await chunkUploadCompleted(fileElement, response.value); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/woltlab-core-file.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/woltlab-core-file.js index 21a4cb7fb9..87a725759b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/woltlab-core-file.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/woltlab-core-file.js @@ -21,9 +21,11 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, #data = undefined; #filename = ""; #fileId = undefined; + #fileSize = undefined; #link = undefined; #mimeType = undefined; #state = 0 /* State.Initial */; + #validationError = undefined; #thumbnails = []; #readyReject; #readyResolve; @@ -49,8 +51,10 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, // Files that exist at page load have a valid file id, otherwise a new // file element can only be the result of an upload attempt. if (this.#fileId === undefined) { - this.#filename = this.dataset.filename || "bogus.bin"; + this.#filename = this.dataset.filename || "unknown.bin"; delete this.dataset.filename; + this.#fileSize = parseInt(this.dataset.fileSize || "0"); + delete this.dataset.fileSize; this.#mimeType = this.dataset.mimeType || "application/octet-stream"; delete this.dataset.mimeType; const fileId = parseInt(this.getAttribute("file-id") || "0"); @@ -97,7 +101,7 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, } break; case 4 /* State.Failed */: - this.#replaceWithIcon("times"); + this.#replaceWithIcon("triangle-exclamation"); break; default: throw new Error("Unreachable", { @@ -174,6 +178,9 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, get filename() { return this.#filename; } + get fileSize() { + return this.#fileSize; + } get mimeType() { return this.#mimeType; } @@ -197,11 +204,12 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, return false; } } - uploadFailed() { + uploadFailed(validationError) { if (this.#state !== 1 /* State.Uploading */) { return; } this.#state = 4 /* State.Failed */; + this.#validationError = validationError; this.#rebuildElement(); this.#readyReject(); } @@ -246,6 +254,9 @@ define(["require", "exports", "WoltLabSuite/Core/FileUtil"], function (require, get ready() { return this.#readyPromise; } + get validationError() { + return this.#validationError; + } } exports.WoltlabCoreFileElement = WoltlabCoreFileElement; exports.default = WoltlabCoreFileElement; diff --git a/wcfsetup/install/files/lib/data/file/File.class.php b/wcfsetup/install/files/lib/data/file/File.class.php index c77b2b0262..a6df5d4684 100644 --- a/wcfsetup/install/files/lib/data/file/File.class.php +++ b/wcfsetup/install/files/lib/data/file/File.class.php @@ -148,6 +148,7 @@ class File extends DatabaseObject fileID, StringUtil::encodeHTML($this->filename), + $this->fileSize, StringUtil::encodeHTML($this->mimeType), StringUtil::encodeHTML(\json_encode($thumbnails)), StringUtil::encodeHTML(\json_encode($metaData)), diff --git a/wcfsetup/install/files/style/ui/attachment.scss b/wcfsetup/install/files/style/ui/attachment.scss index c7ed608a63..8ee0de940d 100644 --- a/wcfsetup/install/files/style/ui/attachment.scss +++ b/wcfsetup/install/files/style/ui/attachment.scss @@ -139,7 +139,7 @@ html[data-color-scheme="dark"] { .attachment__list { display: grid; gap: 10px; - grid-auto-flow: column; + grid-auto-flow: row; // TODO: use container queries to make this more dynamic? grid-template-columns: repeat(3, 1fr); } @@ -153,16 +153,27 @@ html[data-color-scheme="dark"] { border-radius: var(--wcfBorderRadius); box-shadow: var(--wcfBoxShadowCard); display: grid; - gap: 10px; grid-template-areas: "file filename" + "file fileSize" "file buttons"; - grid-template-columns: 64px auto; + grid-template-columns: 80px auto; padding: 10px; } +.attachment__item--error { + border-color: var(--wcfStatusErrorBorder); +} + +.attachment__item--error .attachment__item__file { + color: var(--wcfStatusErrorText); +} + .attachment__item__file { + display: flex; grid-area: file; + justify-content: center; + margin-right: 10px; } .attachment__item__filename { @@ -173,7 +184,17 @@ html[data-color-scheme="dark"] { white-space: nowrap; } +.attachment__item__fileSize { + color: var(--wcfContentDimmedText); + font-size: 12px; + grid-area: fileSize; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .attachment__item__buttons { + align-items: end; column-gap: 5px; display: flex; grid-area: buttons;