From 8f0b9117b79905af7737374c15df871d80411d26 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Thu, 22 Aug 2024 13:32:34 -0300 Subject: [PATCH] Dispatch direct-upload events on attachment uploads When using Action Text's rich textarea, it's possible to attach files to the editor. Previously, that action didn't dispatch any events, which made it hard to react to the file uploads. For instance, if an upload failed, there was no way to notify the user about it, or remove the attachment from the editor. This commits adds new events - `direct-upload:start`, `direct-upload:progress`, and `direct-upload:end` - similar to how Active Storage's direct uploads work. Closes #37793 Supersedes #37794 Co-authored-by: Brad Rees --- actiontext/CHANGELOG.md | 12 +++++++ .../app/assets/javascripts/actiontext.esm.js | 32 ++++++++++++++++--- .../app/assets/javascripts/actiontext.js | 32 ++++++++++++++++--- .../actiontext/attachment_upload.js | 31 ++++++++++++++---- .../assets/javascripts/activestorage.esm.js | 2 +- .../app/assets/javascripts/activestorage.js | 1 + .../app/javascript/activestorage/index.js | 3 +- guides/source/action_text_overview.md | 9 +++++- 8 files changed, 102 insertions(+), 20 deletions(-) diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md index ee979f24eb560..a043b5c86cccd 100644 --- a/actiontext/CHANGELOG.md +++ b/actiontext/CHANGELOG.md @@ -1,3 +1,15 @@ +* Dispatch direct-upload events on attachment uploads + + When using Action Text's rich textarea, it's possible to attach files to the + editor. Previously, that action didn't dispatch any events, which made it hard + to react to the file uploads. For instance, if an upload failed, there was no + way to notify the user about it, or remove the attachment from the editor. + + This commits adds new events - `direct-upload:start`, `direct-upload:progress`, + and `direct-upload:end` - similar to how Active Storage's direct uploads work. + + *Matheus Richard*, *Brad Rees* + * Add `store_if_blank` option to `has_rich_text` Pass `store_if_blank: false` to not create `ActionText::RichText` records when saving with a blank attribute, such as from an optional form parameter. diff --git a/actiontext/app/assets/javascripts/actiontext.esm.js b/actiontext/app/assets/javascripts/actiontext.esm.js index 2f433b7f0a83b..79028350380a3 100644 --- a/actiontext/app/assets/javascripts/actiontext.esm.js +++ b/actiontext/app/assets/javascripts/actiontext.esm.js @@ -853,25 +853,47 @@ class AttachmentUpload { } start() { this.directUpload.create(this.directUploadDidComplete.bind(this)); + this.dispatch("start"); } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", (event => { const progress = event.loaded / event.total * 100; this.attachment.setUploadProgress(progress); + if (progress) { + this.dispatch("progress", { + progress: progress + }); + } })); } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`); + this.dispatchError(error); + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }); + this.dispatch("end"); } - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }); } createBlobUrl(signedId, filename) { return this.blobUrlTemplate.replace(":signed_id", signedId).replace(":filename", encodeURIComponent(filename)); } + dispatch(name, detail = {}) { + detail.attachment = this.attachment; + return dispatchEvent(this.element, `direct-upload:${name}`, { + detail: detail + }); + } + dispatchError(error) { + const event = this.dispatch("error", { + error: error + }); + if (!event.defaultPrevented) { + alert(error); + } + } get directUploadUrl() { return this.element.dataset.directUploadUrl; } diff --git a/actiontext/app/assets/javascripts/actiontext.js b/actiontext/app/assets/javascripts/actiontext.js index ef283f27577e7..e61f03ce103fa 100644 --- a/actiontext/app/assets/javascripts/actiontext.js +++ b/actiontext/app/assets/javascripts/actiontext.js @@ -826,25 +826,47 @@ } start() { this.directUpload.create(this.directUploadDidComplete.bind(this)); + this.dispatch("start"); } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", (event => { const progress = event.loaded / event.total * 100; this.attachment.setUploadProgress(progress); + if (progress) { + this.dispatch("progress", { + progress: progress + }); + } })); } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`); + this.dispatchError(error); + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }); + this.dispatch("end"); } - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }); } createBlobUrl(signedId, filename) { return this.blobUrlTemplate.replace(":signed_id", signedId).replace(":filename", encodeURIComponent(filename)); } + dispatch(name, detail = {}) { + detail.attachment = this.attachment; + return dispatchEvent(this.element, `direct-upload:${name}`, { + detail: detail + }); + } + dispatchError(error) { + const event = this.dispatch("error", { + error: error + }); + if (!event.defaultPrevented) { + alert(error); + } + } get directUploadUrl() { return this.element.dataset.directUploadUrl; } diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js index 77fbc97df64db..d2c0a3af2e15c 100644 --- a/actiontext/app/javascript/actiontext/attachment_upload.js +++ b/actiontext/app/javascript/actiontext/attachment_upload.js @@ -1,4 +1,4 @@ -import { DirectUpload } from "@rails/activestorage" +import { DirectUpload, dispatchEvent } from "@rails/activestorage" export class AttachmentUpload { constructor(attachment, element) { @@ -9,24 +9,29 @@ export class AttachmentUpload { start() { this.directUpload.create(this.directUploadDidComplete.bind(this)) + this.dispatch("start") } directUploadWillStoreFileWithXHR(xhr) { xhr.upload.addEventListener("progress", event => { const progress = event.loaded / event.total * 100 this.attachment.setUploadProgress(progress) + if (progress) { + this.dispatch("progress", { progress: progress }) + } }) } directUploadDidComplete(error, attributes) { if (error) { - throw new Error(`Direct upload failed: ${error}`) + this.dispatchError(error) + } else { + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }) + this.dispatch("end") } - - this.attachment.setAttributes({ - sgid: attributes.attachable_sgid, - url: this.createBlobUrl(attributes.signed_id, attributes.filename) - }) } createBlobUrl(signedId, filename) { @@ -35,6 +40,18 @@ export class AttachmentUpload { .replace(":filename", encodeURIComponent(filename)) } + dispatch(name, detail = {}) { + detail.attachment = this.attachment + return dispatchEvent(this.element, `direct-upload:${name}`, { detail }) + } + + dispatchError(error) { + const event = this.dispatch("error", { error }) + if (!event.defaultPrevented) { + alert(error); + } + } + get directUploadUrl() { return this.element.dataset.directUploadUrl } diff --git a/activestorage/app/assets/javascripts/activestorage.esm.js b/activestorage/app/assets/javascripts/activestorage.esm.js index e6d60e64a6f2e..1ac98355eccb5 100644 --- a/activestorage/app/assets/javascripts/activestorage.esm.js +++ b/activestorage/app/assets/javascripts/activestorage.esm.js @@ -845,4 +845,4 @@ function autostart() { setTimeout(autostart, 1); -export { DirectUpload, DirectUploadController, DirectUploadsController, start }; +export { DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent, start }; diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index a31bf08fbf433..754c727bc8970 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -822,6 +822,7 @@ exports.DirectUpload = DirectUpload; exports.DirectUploadController = DirectUploadController; exports.DirectUploadsController = DirectUploadsController; + exports.dispatchEvent = dispatchEvent; exports.start = start; Object.defineProperty(exports, "__esModule", { value: true diff --git a/activestorage/app/javascript/activestorage/index.js b/activestorage/app/javascript/activestorage/index.js index d8e3f150c4f25..4f15c19ba6118 100644 --- a/activestorage/app/javascript/activestorage/index.js +++ b/activestorage/app/javascript/activestorage/index.js @@ -2,7 +2,8 @@ import { start } from "./ujs" import { DirectUpload } from "./direct_upload" import { DirectUploadController } from "./direct_upload_controller" import { DirectUploadsController } from "./direct_uploads_controller" -export { start, DirectUpload, DirectUploadController, DirectUploadsController } +import { dispatchEvent } from "./helpers" +export { start, DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent } function autostart() { if (window.ActiveStorage) { diff --git a/guides/source/action_text_overview.md b/guides/source/action_text_overview.md index 16a5bc1df2319..2ed2afd3bcbfa 100644 --- a/guides/source/action_text_overview.md +++ b/guides/source/action_text_overview.md @@ -233,7 +233,6 @@ To customize the HTML rendered for embedded images and other attachments (known as blobs), edit the `app/views/active_storage/blobs/_blob.html.erb` template created by the installer: - ```html+erb <%# app/views/active_storage/blobs/_blob.html.erb %>
attachment--<%= blob.filename.extension %>"> @@ -270,6 +269,14 @@ encounter when working with Action Text and Active Storage is that images do not render correctly in the editor. This is usually due to the `libvips` dependency not being installed. +#### Attachment Direct Upload JavaScript Events + +| Event name | Event target | Event data (`event.detail`) | Description | +| --- | --- | --- | --- | +| `direct-upload:start` | `` | `{id, file}` | A direct upload is starting. | +| `direct-upload:progress` | `` | `{id, file, progress}` | As requests to store files progress. | +| `direct-upload:error` | `` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. | +| `direct-upload:end` | `` | `{id, file}` | A direct upload has ended. | ### Signed GlobalID