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 <github@bradleyrees.com>
This commit is contained in:
Matheus Richard 2024-08-22 13:32:34 -03:00
parent 10924d3c96
commit 8f0b9117b7
8 changed files with 102 additions and 20 deletions

View File

@ -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.

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -845,4 +845,4 @@ function autostart() {
setTimeout(autostart, 1);
export { DirectUpload, DirectUploadController, DirectUploadsController, start };
export { DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent, start };

View File

@ -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

View File

@ -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) {

View File

@ -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 %>
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> 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` | `<input>` | `{id, file}` | A direct upload is starting. |
| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
### Signed GlobalID