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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4231d75f98a1affd65c85e91421526be46fd90a969e70909e89a34a515f43727
4
- data.tar.gz: 2653d547bdaa5dc5177b0eacfb1a7918ce4f22398e5df62ce2ce052db4632947
3
+ metadata.gz: eb0a5828862fbe5feb5e9da0492d9e23dc3698a099168c09e04a90e83f40c493
4
+ data.tar.gz: f868efb0bb5eac969ecb0d6ed809c5385b7c8111b6c68e43ec64a8f6e91c7ae1
5
5
  SHA512:
6
- metadata.gz: 634da0a3393a99090235047810cfbbda78325036b2b84dd288327121e194c9035fe560938153d8614c90014be66603ec47b7eb061473d9924a41e2dc19824215
7
- data.tar.gz: d744c1ee748c6444dc1bf8beae7bdc8d7bf355a4ced26227be0e06b8107d155e06d36b41f14f72e1d74ca0af938e78827db4a3486dbba160d9d85521d86030f9
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.validationError = error;
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: "b4ee8d4972ea0ed8406c9fe96c23dfc6ee48a980" }, h("slot", { key: "c8d48faeaf8a66f7dce8f28ba762fc1d4e568443" }), h("figure", { key: "1afd377c02f883e31cd4346c3f840c86c5fe9199" }, h("div", { key: "7c05e3b92dd5bd1598970ff2682d5a7fd3abcc05", class: "progress-details" }, h("progress-bar", { key: "c2b763ddb9b0de0f8bd95d9cc0f7b3f96763d810", percent: this.percent, class: this.state }, h("a", { key: "0426c1fcfc5f0f418e4fabbace84a6defaa4e4fb", class: "download-link", href: this.src, download: this.filename, onClick: (e) => e.stopPropagation() }, this.filename)), h("span", { key: "cc0accb102d91e5bb2dd84e97123675fa0c96afb", class: "progress-icon" }), h("a", { key: "830973ccad0e991b72868a2f33c84e1c772b3907", class: "remove-media", onClick: this.removeClicked, href: "#" }, h("span", { key: "5f8f67d06ff5e85faa9ae5642c09a646ab5edbe3" }, "Remove media"))), this.validationError ? h("p", { class: "validation-error" }, this.validationError) : "", this.preview ? h("attachment-preview", { src: this.src, filetype: this.filetype }) : ""));
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 = this.directupload;
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.validationError = error
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 = this.directupload
321
+ attachmentFile.setAttribute("url", this.directupload)
322
322
  attachmentFile.accepts = this.accepts
323
323
  attachmentFile.max = this.max
324
324
  attachmentFile.file = file
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module AttachmentField
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
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.1.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-01-03 00:00:00.000000000 Z
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