bard-attachment_field 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +78 -0
- data/app/assets/javascripts/input-attachment.js +25 -6
- data/input-attachment/src/components/attachment-file/attachment-file.css +12 -0
- data/input-attachment/src/components/attachment-file/attachment-file.tsx +19 -2
- data/input-attachment/src/components/input-attachment/form-controller.tsx +12 -2
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +1 -1
- data/lib/bard/attachment_field/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb0a5828862fbe5feb5e9da0492d9e23dc3698a099168c09e04a90e83f40c493
|
|
4
|
+
data.tar.gz: f868efb0bb5eac969ecb0d6ed809c5385b7c8111b6c68e43ec64a8f6e91c7ae1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36b03a42ab7d9820fcb049f3406dbe2fe47bb57c13d73b2c102f2373194ed23eb22dee20360ada23f2d9618fa29cf789973702aae22f28a346212b39c11c26c7
|
|
7
|
+
data.tar.gz: 24f650bbdc0c0468a371e579255b52cc5a18cbfa5511d92e17671a9903383ad62e9f4ca9abbd73b9ca2c1b5997e4b895f1cb46a1a357f1a3e8f48be93f73f64d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Prevent form submission when direct uploads fail, with retry button
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- Re-enable input-attachment after removing a file
|
|
12
|
+
- Fix direct upload URL race condition on disconnected Stencil elements
|
|
13
|
+
|
|
3
14
|
## [0.1.0] - 2026-01-03
|
|
4
15
|
|
|
5
16
|
Initial release of bard-attachment_field, a rewrite of bard-file_field.
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
`bard-attachment_field` is a Ruby gem that provides an enhanced file upload field for Rails forms, powered by Stencil web components. It adds `form.attachment_field :avatar` to Rails form builders, rendering `<input-attachment>` web components with drag-and-drop uploads, image/video previews, and ActiveStorage direct upload integration.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
### Ruby/Rails (gem)
|
|
12
|
+
|
|
13
|
+
- **Run all tests**: `bundle exec cucumber features/`
|
|
14
|
+
- **Run a single feature**: `bundle exec cucumber features/validations.feature`
|
|
15
|
+
- **Run a specific scenario**: `bundle exec cucumber features/validations.feature:42`
|
|
16
|
+
- **Default rake task** (runs cucumber): `rake`
|
|
17
|
+
|
|
18
|
+
### JavaScript (web components)
|
|
19
|
+
|
|
20
|
+
The web components live in `input-attachment/` and use **Bun** (not npm).
|
|
21
|
+
|
|
22
|
+
- **Install deps**: `cd input-attachment && bun install`
|
|
23
|
+
- **Build**: `cd input-attachment && bun run build`
|
|
24
|
+
- **Run JS tests**: `cd input-attachment && bun run test`
|
|
25
|
+
- **Dev server with watch**: `cd input-attachment && bun start`
|
|
26
|
+
|
|
27
|
+
JavaScript must be built before running Cucumber tests — the built bundle at `app/assets/javascripts/input-attachment.js` is served by Sprockets in the test app.
|
|
28
|
+
|
|
29
|
+
### Multi-Rails testing
|
|
30
|
+
|
|
31
|
+
Uses Appraisal with gemfiles in `gemfiles/` for Rails 7.1, 7.2, 8.0, and 8.1. CI runs the matrix against Ruby 3.2/3.3/3.4.
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
### Two-part project
|
|
36
|
+
|
|
37
|
+
1. **Ruby gem** (`lib/bard/attachment_field/`) — Rails Engine that adds `attachment_field` to form builders
|
|
38
|
+
2. **Stencil web components** (`input-attachment/`) — TypeScript components compiled to a JS bundle
|
|
39
|
+
|
|
40
|
+
### Ruby side
|
|
41
|
+
|
|
42
|
+
- `Engine` (`lib/bard/attachment_field.rb`) — mixes `FormBuilder` into the default form builder on `after_initialize`
|
|
43
|
+
- `FormBuilder` (`lib/bard/attachment_field/form_builder.rb`) — adds `attachment_field` method
|
|
44
|
+
- `Field` (`lib/bard/attachment_field/field.rb`) — inherits from `ActionView::Helpers::Tags::TextField`, renders `<input-attachment>` element with existing `<attachment-file>` children for persisted attachments
|
|
45
|
+
|
|
46
|
+
### Web components side (`input-attachment/src/components/`)
|
|
47
|
+
|
|
48
|
+
- `<input-attachment>` — main component, manages file state, drag/drop, validation
|
|
49
|
+
- `<attachment-file>` — individual file with preview, upload state, removal
|
|
50
|
+
- `<attachment-preview>` — preview display
|
|
51
|
+
- `FormController` — coordinates upload queue on form submit
|
|
52
|
+
- `DirectUploadController` — handles Rails ActiveStorage direct uploads
|
|
53
|
+
|
|
54
|
+
### Upload flow
|
|
55
|
+
|
|
56
|
+
1. User selects/drops files → `<attachment-file>` components created
|
|
57
|
+
2. On form submit, `FormController` queues uploads
|
|
58
|
+
3. `DirectUploadController` uploads each file to ActiveStorage
|
|
59
|
+
4. Completed uploads provide signed IDs for form submission
|
|
60
|
+
|
|
61
|
+
### Test app
|
|
62
|
+
|
|
63
|
+
The Cucumber test suite uses a self-contained Rails app defined in `features/support/app.rb` with:
|
|
64
|
+
- SQLite database at `tmp/test_app/tmp/test.db`
|
|
65
|
+
- `Post` model with `has_one_attached :image`, `has_many_attached :images`, etc.
|
|
66
|
+
- Controllers/views in `features/support/`
|
|
67
|
+
- Capybara with Cuprite (headless Chrome) driver
|
|
68
|
+
- `capybara-shadowdom` for interacting with shadow DOM elements
|
|
69
|
+
|
|
70
|
+
### Cucumber test helpers
|
|
71
|
+
|
|
72
|
+
`lib/bard/attachment_field/cucumber.rb` provides:
|
|
73
|
+
- Step definitions for attaching, removing, dragging files and checking previews/validation
|
|
74
|
+
- `Bard::AttachmentField::TestHelper` with `attach_files`, `wait_for_upload`, `find_field`
|
|
75
|
+
- Chop integration (`Chop::Form::AttachmentField`) for form table diffing/filling
|
|
76
|
+
- CDP workaround for shadow DOM file inputs (creates temp regular DOM input, transfers files via JS)
|
|
77
|
+
|
|
78
|
+
Consumer apps can `require "bard/attachment_field/cucumber"` and configure `TestHelper.fixtures_path`.
|
|
@@ -5267,7 +5267,7 @@ var request = (verb, url, payload, headers) => {
|
|
|
5267
5267
|
});
|
|
5268
5268
|
};
|
|
5269
5269
|
var get = (url, payload = {}, headers = {}) => request("get", url, payload, headers);
|
|
5270
|
-
var attachmentFileCss = `:host{display:block;width:100%;max-width:100%;font-size:13px}figure{margin:0}.progress-details{position:relative;display:flex;align-items:center}progress-bar{flex:1 0;padding:0 10px}progress-bar.pending{opacity:0.5}progress-bar.complete{opacity:0.8}progress-bar:not(.complete)+.progress-icon{display:none}progress-bar.complete+.progress-icon{content:url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"><g><path d="M6.3,9.1c0.2,0,0.5,0.1,0.7,0.4c0.5,0.5,1,1,1.4,1.4c0.3,0.3,0.3,0.3,0.6,0c1.4-1.3,2.7-2.6,4-3.9c0.3-0.3,0.6-0.4,1-0.4 c0.5,0.1,0.9,0.6,0.7,1.1c-0.1,0.2-0.2,0.4-0.3,0.6c-1.6,1.6-3.2,3.2-4.8,4.8c-0.5,0.5-1,0.5-1.6,0c-0.8-0.7-1.5-1.5-2.3-2.3 c-0.3-0.3-0.5-0.6-0.3-1.1C5.5,9.3,5.8,9.1,6.3,9.1z"/></g></svg>');filter:invert(100%)}.progress-icon{display:inline-block;flex:0 0 20px;width:28px;height:28px;background-size:contain;position:absolute;right:30px;z-index:1}progress-bar.error{background:#f8b3b1;background:rgba(74, 70, 70, 0.25);opacity:1}.progress-bar a{color:#fff}.download-link{padding-right:20px;color:#fff}.remove-media{display:inline-block;content:url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><g><path d="M0,19.9C0.2,8.5,9.2-0.1,20.1,0C31.8,0.1,40.2,9.5,40,20.4c-0.2,11-8.9,19.7-20.1,19.6C8,39.9,0,30.5,0,19.9z M20,3.7 c-9,0-16.3,7-16.3,16.2C3.7,29,10.9,36.3,20,36.3c9,0,16.3-7.1,16.4-16.3C36.3,11,29.2,3.8,20,3.7z"/><path d="M17.3,20c-0.2-0.2-0.3-0.4-0.5-0.6c-1-1-2-1.9-2.9-2.9c-0.5-0.5-0.8-1.1-0.7-1.9c0.1-0.7,0.5-1.2,1.2-1.4 c0.8-0.2,1.5,0,2.1,0.6c1,1,2,2,3,3.1c0.3,0.4,0.6,0.3,0.9,0c1-1,2-2,3-3c0.3-0.3,0.7-0.5,1.1-0.6c0.8-0.2,1.6,0.1,2,0.8 c0.4,0.8,0.3,1.7-0.4,2.4c-1,1-2,2-3,3c-0.2,0.2-0.3,0.4-0.5,0.6c1.2,1.2,2.3,2.3,3.4,3.4c0.6,0.6,0.9,1.3,0.6,2.2 c-0.4,1.1-1.7,1.6-2.6,1c-0.3-0.2-0.5-0.4-0.8-0.6c-1-1-1.9-1.9-2.9-2.9c-0.3-0.3-0.5-0.3-0.9,0c-1,1-2,2.1-3,3 c-0.4,0.4-1,0.6-1.5,0.8c-0.6,0.1-1.2-0.2-1.5-0.8c-0.4-0.6-0.5-1.3-0.1-1.9c0.2-0.3,0.4-0.5,0.6-0.7C15.1,22.3,16.2,21.2,17.3,20z "/></g></svg>');flex:0 0 25px;width:25px;height:20px;align-items:center;opacity:0.25}.remove-media:hover{opacity:1;filter:invert(50%)sepia(100%)saturate(10000%)}.remove-media span{display:inline-block;text-indent:-9999px;color:transparent}.validation-error{color:#c00;font-size:12px;margin:4px 0 0 10px}`;
|
|
5270
|
+
var attachmentFileCss = `:host{display:block;width:100%;max-width:100%;font-size:13px}figure{margin:0}.progress-details{position:relative;display:flex;align-items:center}progress-bar{flex:1 0;padding:0 10px}progress-bar.pending{opacity:0.5}progress-bar.complete{opacity:0.8}progress-bar:not(.complete)+.progress-icon{display:none}progress-bar.complete+.progress-icon{content:url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"><g><path d="M6.3,9.1c0.2,0,0.5,0.1,0.7,0.4c0.5,0.5,1,1,1.4,1.4c0.3,0.3,0.3,0.3,0.6,0c1.4-1.3,2.7-2.6,4-3.9c0.3-0.3,0.6-0.4,1-0.4 c0.5,0.1,0.9,0.6,0.7,1.1c-0.1,0.2-0.2,0.4-0.3,0.6c-1.6,1.6-3.2,3.2-4.8,4.8c-0.5,0.5-1,0.5-1.6,0c-0.8-0.7-1.5-1.5-2.3-2.3 c-0.3-0.3-0.5-0.6-0.3-1.1C5.5,9.3,5.8,9.1,6.3,9.1z"/></g></svg>');filter:invert(100%)}.progress-icon{display:inline-block;flex:0 0 20px;width:28px;height:28px;background-size:contain;position:absolute;right:30px;z-index:1}progress-bar.error{background:#f8b3b1;background:rgba(74, 70, 70, 0.25);opacity:1}.progress-bar a{color:#fff}.download-link{padding-right:20px;color:#fff}.remove-media{display:inline-block;content:url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><g><path d="M0,19.9C0.2,8.5,9.2-0.1,20.1,0C31.8,0.1,40.2,9.5,40,20.4c-0.2,11-8.9,19.7-20.1,19.6C8,39.9,0,30.5,0,19.9z M20,3.7 c-9,0-16.3,7-16.3,16.2C3.7,29,10.9,36.3,20,36.3c9,0,16.3-7.1,16.4-16.3C36.3,11,29.2,3.8,20,3.7z"/><path d="M17.3,20c-0.2-0.2-0.3-0.4-0.5-0.6c-1-1-2-1.9-2.9-2.9c-0.5-0.5-0.8-1.1-0.7-1.9c0.1-0.7,0.5-1.2,1.2-1.4 c0.8-0.2,1.5,0,2.1,0.6c1,1,2,2,3,3.1c0.3,0.4,0.6,0.3,0.9,0c1-1,2-2,3-3c0.3-0.3,0.7-0.5,1.1-0.6c0.8-0.2,1.6,0.1,2,0.8 c0.4,0.8,0.3,1.7-0.4,2.4c-1,1-2,2-3,3c-0.2,0.2-0.3,0.4-0.5,0.6c1.2,1.2,2.3,2.3,3.4,3.4c0.6,0.6,0.9,1.3,0.6,2.2 c-0.4,1.1-1.7,1.6-2.6,1c-0.3-0.2-0.5-0.4-0.8-0.6c-1-1-1.9-1.9-2.9-2.9c-0.3-0.3-0.5-0.3-0.9,0c-1,1-2,2.1-3,3 c-0.4,0.4-1,0.6-1.5,0.8c-0.6,0.1-1.2-0.2-1.5-0.8c-0.4-0.6-0.5-1.3-0.1-1.9c0.2-0.3,0.4-0.5,0.6-0.7C15.1,22.3,16.2,21.2,17.3,20z "/></g></svg>');flex:0 0 25px;width:25px;height:20px;align-items:center;opacity:0.25}.remove-media:hover{opacity:1;filter:invert(50%)sepia(100%)saturate(10000%)}.remove-media span{display:inline-block;text-indent:-9999px;color:transparent}.retry-media{color:#c00;font-size:12px;text-decoration:underline;cursor:pointer;padding-left:8px}.retry-media:hover{color:#900}.validation-error{color:#c00;font-size:12px;margin:4px 0 0 10px}`;
|
|
5271
5271
|
var AttachmentFile = /* @__PURE__ */ proxyCustomElement(class AttachmentFile2 extends H {
|
|
5272
5272
|
constructor(registerHost2) {
|
|
5273
5273
|
super();
|
|
@@ -5302,9 +5302,20 @@ var AttachmentFile = /* @__PURE__ */ proxyCustomElement(class AttachmentFile2 ex
|
|
|
5302
5302
|
this.controller?.cancel();
|
|
5303
5303
|
this.removeEvent.emit(this);
|
|
5304
5304
|
};
|
|
5305
|
+
retryClicked = (event) => {
|
|
5306
|
+
event.stopPropagation();
|
|
5307
|
+
event.preventDefault();
|
|
5308
|
+
this.state = "pending";
|
|
5309
|
+
this.percent = 0;
|
|
5310
|
+
this.validationError = "";
|
|
5311
|
+
this.uploadError = "";
|
|
5312
|
+
this.controller = new DirectUploadController2(this.el, this._file);
|
|
5313
|
+
this.controller.dispatch("initialize", { controller: this.controller });
|
|
5314
|
+
};
|
|
5305
5315
|
controller;
|
|
5306
5316
|
_file;
|
|
5307
5317
|
validationError = "";
|
|
5318
|
+
uploadError = "";
|
|
5308
5319
|
componentWillLoad() {
|
|
5309
5320
|
this.setMissingFiletype();
|
|
5310
5321
|
}
|
|
@@ -5348,7 +5359,7 @@ var AttachmentFile = /* @__PURE__ */ proxyCustomElement(class AttachmentFile2 ex
|
|
|
5348
5359
|
event.preventDefault();
|
|
5349
5360
|
const { error } = event.detail;
|
|
5350
5361
|
this.state = "error";
|
|
5351
|
-
this.
|
|
5362
|
+
this.uploadError = error;
|
|
5352
5363
|
}
|
|
5353
5364
|
end(_event) {
|
|
5354
5365
|
if (this.state !== "error") {
|
|
@@ -5357,7 +5368,7 @@ var AttachmentFile = /* @__PURE__ */ proxyCustomElement(class AttachmentFile2 ex
|
|
|
5357
5368
|
}
|
|
5358
5369
|
}
|
|
5359
5370
|
render() {
|
|
5360
|
-
return h(Host, { key: "
|
|
5371
|
+
return h(Host, { key: "048e2ff577fe17addf864b61d83a039108016a12" }, h("slot", { key: "8be67080894acaff712da3384fc8d801a29ec577" }), h("figure", { key: "272c3a327466f2da4654e13627cdd724b5f0cd6a" }, h("div", { key: "cd4d689a99ee9f24c1440905e9c127b5aca2d54f", class: "progress-details" }, h("progress-bar", { key: "b96ddb30bf8bec245ebd2905df0eca909774a9f0", percent: this.percent, class: this.state }, h("a", { key: "a7999fb28b349a78705d8d3babc57d731e7161b4", class: "download-link", href: this.src, download: this.filename, onClick: (e) => e.stopPropagation() }, this.filename)), h("span", { key: "ea430736922cbb19e797796555be2316fc4a7615", class: "progress-icon" }), h("a", { key: "ad2af0cbdd7fffb8fd1b3250d82fa6e2f46c0147", class: "remove-media", onClick: this.removeClicked, href: "#" }, h("span", { key: "5a1bfd7c33320b96472910b127c9f1d8c124e004" }, "Remove media")), this.uploadError && this._file ? h("a", { class: "retry-media", onClick: this.retryClicked, href: "#" }, h("span", null, "Retry upload")) : ""), this.validationError || this.uploadError ? h("p", { class: "validation-error" }, this.validationError || this.uploadError) : "", this.preview ? h("attachment-preview", { src: this.src, filetype: this.filetype }) : ""));
|
|
5361
5372
|
}
|
|
5362
5373
|
componentDidLoad() {
|
|
5363
5374
|
if (this.state == "pending" && this._file) {
|
|
@@ -5421,14 +5432,12 @@ var FormController = class _FormController {
|
|
|
5421
5432
|
controllers;
|
|
5422
5433
|
submitted;
|
|
5423
5434
|
processing;
|
|
5424
|
-
errors;
|
|
5425
5435
|
constructor(form) {
|
|
5426
5436
|
this.element = form;
|
|
5427
5437
|
this.progressTargetMap = {};
|
|
5428
5438
|
this.controllers = [];
|
|
5429
5439
|
this.submitted = false;
|
|
5430
5440
|
this.processing = false;
|
|
5431
|
-
this.errors = false;
|
|
5432
5441
|
this.element.insertAdjacentHTML("beforeend", `<dialog id="form-controller-dialog">
|
|
5433
5442
|
<div class="direct-upload-wrapper">
|
|
5434
5443
|
<div class="direct-upload-content">
|
|
@@ -5482,8 +5491,16 @@ var FormController = class _FormController {
|
|
|
5482
5491
|
this.submitForm();
|
|
5483
5492
|
}
|
|
5484
5493
|
}
|
|
5494
|
+
hasUploadErrors() {
|
|
5495
|
+
return Array.from(this.element.querySelectorAll("attachment-file")).some((el) => el.state === "error");
|
|
5496
|
+
}
|
|
5485
5497
|
submitForm() {
|
|
5486
5498
|
if (this.submitted) {
|
|
5499
|
+
if (this.hasUploadErrors()) {
|
|
5500
|
+
this.dialog.close();
|
|
5501
|
+
this.setInputAttachmentsDisabled(false);
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5487
5504
|
this.setInputAttachmentsDisabled(true);
|
|
5488
5505
|
window.setTimeout(() => {
|
|
5489
5506
|
this.element.submit();
|
|
@@ -5529,6 +5546,8 @@ var FormController = class _FormController {
|
|
|
5529
5546
|
document.getElementById(`direct-upload-${id2}`).remove();
|
|
5530
5547
|
delete this.progressTargetMap[id2];
|
|
5531
5548
|
}
|
|
5549
|
+
this.setInputAttachmentsDisabled(false);
|
|
5550
|
+
requestAnimationFrame(() => this.submitForm());
|
|
5532
5551
|
}
|
|
5533
5552
|
};
|
|
5534
5553
|
function arrayRemove(arr, e) {
|
|
@@ -5935,7 +5954,7 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5935
5954
|
const attachmentFile = document.createElement("attachment-file");
|
|
5936
5955
|
attachmentFile.name = this.name;
|
|
5937
5956
|
attachmentFile.preview = this.preview;
|
|
5938
|
-
attachmentFile.url
|
|
5957
|
+
attachmentFile.setAttribute("url", this.directupload);
|
|
5939
5958
|
attachmentFile.accepts = this.accepts;
|
|
5940
5959
|
attachmentFile.max = this.max;
|
|
5941
5960
|
attachmentFile.file = file;
|
|
@@ -82,6 +82,18 @@ progress-bar.error{
|
|
|
82
82
|
color: transparent;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
.retry-media{
|
|
86
|
+
color: #c00;
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
text-decoration: underline;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
padding-left: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.retry-media:hover{
|
|
94
|
+
color: #900;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
.validation-error{
|
|
86
98
|
color: #c00;
|
|
87
99
|
font-size: 12px;
|
|
@@ -40,9 +40,21 @@ export class AttachmentFile {
|
|
|
40
40
|
this.removeEvent.emit(this)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
private retryClicked = event => {
|
|
44
|
+
event.stopPropagation()
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
this.state = "pending"
|
|
47
|
+
this.percent = 0
|
|
48
|
+
this.validationError = ""
|
|
49
|
+
this.uploadError = ""
|
|
50
|
+
this.controller = new DirectUploadController(this.el, this._file)
|
|
51
|
+
this.controller.dispatch("initialize", { controller: this.controller })
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
controller: DirectUploadController
|
|
44
55
|
_file: File
|
|
45
56
|
validationError: string = ""
|
|
57
|
+
uploadError: string = ""
|
|
46
58
|
|
|
47
59
|
componentWillLoad() {
|
|
48
60
|
this.setMissingFiletype()
|
|
@@ -99,7 +111,7 @@ export class AttachmentFile {
|
|
|
99
111
|
event.preventDefault()
|
|
100
112
|
const { error } = event.detail
|
|
101
113
|
this.state = "error"
|
|
102
|
-
this.
|
|
114
|
+
this.uploadError = error
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
@Listen("direct-upload:end")
|
|
@@ -126,8 +138,13 @@ export class AttachmentFile {
|
|
|
126
138
|
<a class="remove-media" onClick={this.removeClicked} href="#">
|
|
127
139
|
<span>Remove media</span>
|
|
128
140
|
</a>
|
|
141
|
+
{this.uploadError && this._file ?
|
|
142
|
+
<a class="retry-media" onClick={this.retryClicked} href="#">
|
|
143
|
+
<span>Retry upload</span>
|
|
144
|
+
</a>
|
|
145
|
+
: ''}
|
|
129
146
|
</div>
|
|
130
|
-
{this.validationError ? <p class="validation-error">{this.validationError}</p> : ''}
|
|
147
|
+
{(this.validationError || this.uploadError) ? <p class="validation-error">{this.validationError || this.uploadError}</p> : ''}
|
|
131
148
|
{this.preview ? <attachment-preview src={this.src} filetype={this.filetype}></attachment-preview> : ''}
|
|
132
149
|
</figure>
|
|
133
150
|
</Host>
|
|
@@ -13,7 +13,6 @@ export default class FormController {
|
|
|
13
13
|
controllers: Array<DirectUploadController>
|
|
14
14
|
submitted: boolean
|
|
15
15
|
processing: boolean
|
|
16
|
-
errors: boolean
|
|
17
16
|
|
|
18
17
|
constructor(form) {
|
|
19
18
|
this.element = form
|
|
@@ -21,7 +20,6 @@ export default class FormController {
|
|
|
21
20
|
this.controllers = []
|
|
22
21
|
this.submitted = false
|
|
23
22
|
this.processing = false
|
|
24
|
-
this.errors = false
|
|
25
23
|
|
|
26
24
|
this.element.insertAdjacentHTML("beforeend",
|
|
27
25
|
`<dialog id="form-controller-dialog">
|
|
@@ -85,8 +83,18 @@ export default class FormController {
|
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
86
|
+
hasUploadErrors() {
|
|
87
|
+
return Array.from(this.element.querySelectorAll("attachment-file"))
|
|
88
|
+
.some((el: any) => el.state === "error")
|
|
89
|
+
}
|
|
90
|
+
|
|
88
91
|
submitForm() {
|
|
89
92
|
if(this.submitted) {
|
|
93
|
+
if(this.hasUploadErrors()) {
|
|
94
|
+
this.dialog.close()
|
|
95
|
+
this.setInputAttachmentsDisabled(false)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
90
98
|
this.setInputAttachmentsDisabled(true)
|
|
91
99
|
window.setTimeout(() => { // allow other async tasks to complete
|
|
92
100
|
this.element.submit()
|
|
@@ -142,5 +150,7 @@ export default class FormController {
|
|
|
142
150
|
document.getElementById(`direct-upload-${id}`).remove()
|
|
143
151
|
delete this.progressTargetMap[id]
|
|
144
152
|
}
|
|
153
|
+
this.setInputAttachmentsDisabled(false)
|
|
154
|
+
requestAnimationFrame(() => this.submitForm())
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -318,7 +318,7 @@ export class InputAttachment {
|
|
|
318
318
|
const attachmentFile = document.createElement('attachment-file') as any
|
|
319
319
|
attachmentFile.name = this.name
|
|
320
320
|
attachmentFile.preview = this.preview
|
|
321
|
-
attachmentFile.url
|
|
321
|
+
attachmentFile.setAttribute("url", this.directupload)
|
|
322
322
|
attachmentFile.accepts = this.accepts
|
|
323
323
|
attachmentFile.max = this.max
|
|
324
324
|
attachmentFile.file = file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bard-attachment_field
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activestorage
|
|
@@ -318,6 +318,7 @@ files:
|
|
|
318
318
|
- ".ruby-version"
|
|
319
319
|
- Appraisals
|
|
320
320
|
- CHANGELOG.md
|
|
321
|
+
- CLAUDE.md
|
|
321
322
|
- Gemfile
|
|
322
323
|
- LICENSE
|
|
323
324
|
- README.md
|