bard-attachment_field 0.6.3 → 0.6.4

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: edba1eefd3a08f4e68003f8fdb54f1c56d182c7fa7861a3a776a0bc03b144720
4
- data.tar.gz: fe58cfe7d8528cb324bcce8b2b5dc5c786a9ab25e8f05cf24673a6f87e6b8542
3
+ metadata.gz: 97544f73074b88b48df66fd5a7aa865a8b88fd4eb8078bfc11ff605973c312ed
4
+ data.tar.gz: f29d06de7233f15966ae9b139d10bf0c048fc02aa3c233af72aadcffdd4cf2b6
5
5
  SHA512:
6
- metadata.gz: d3d9fc5faa9d0d7b195e7c15dce24fc9484644da25df67adb254362927d397ddd409de1a0a8e023f9e91c060633219a9bd3456a20c146973947ebcd38d8deba7
7
- data.tar.gz: 134d8975835697d189f17da80a3aaa1832b115b0b7d632245acd5d29f9ae24a05de4cef61110097dae50bbbbee25465ef6aebc89de74b7dbbb784121792a62f3
6
+ metadata.gz: 118ac9484ef7b4f96a7e2a837fcc38bf3443ca576555ac9d0dcfdab5d16c57074d7927f1e3f454f51c4e2bd83d0fadc60d5810773817825044c7aa5c311dd570
7
+ data.tar.gz: 708a641e9b386a0e91a80f14046711518ca6a9e19a461f894aadefa949c2c36927479a0774fc882a0fea36dbd678f47fb1d787168618e0da76c5a6d943ecdef2
data/Appraisals CHANGED
@@ -14,4 +14,5 @@ end
14
14
 
15
15
  appraise "rails-8.1" do
16
16
  gem "rails", "~> 8.1.0"
17
+ gem "json", "< 3" # ActiveSupport 8.1.3.1 passes JSON.parse options positionally; fixed in 8-1-stable
17
18
  end
@@ -5179,7 +5179,7 @@ var c3 = proxyCustomElement(class extends H {
5179
5179
  updateFormValue() {
5180
5180
  if (!this.name || !this.internals?.setFormValue) return;
5181
5181
  const t2 = new FormData(), e3 = this.files.map(((t3) => t3.value)).filter(((t3) => t3 && "string" != typeof t3 ? (console.error("[input-attachment] Non-string value detected on attachment-file:", typeof t3, t3, Error().stack), false) : "string" == typeof t3 && t3.length > 0));
5182
- if (this.multiple ? (e3.forEach(((e4) => t2.append(this.name, e4))), 0 === e3.length && t2.append(this.name, "")) : t2.set(this.name, e3[0] || ""), this.internals.setFormValue(t2), this.required && 0 === this.files.length) this.internals.setValidity({ valueMissing: true }, "Please select a file.", this.fileInput);
5182
+ if (this.multiple ? (e3.forEach(((e4) => t2.append(this.name, e4))), 0 === e3.length && t2.append(this.name, "")) : t2.set(this.name, e3[0] || ""), this.internals.setFormValue(t2, this.value), this.required && 0 === this.files.length) this.internals.setValidity({ valueMissing: true }, "Please select a file.", this.fileInput);
5183
5183
  else {
5184
5184
  const t3 = this.files.map(((t4) => t4.validationError)).filter(((t4) => t4 && t4.length > 0));
5185
5185
  t3.length > 0 ? this.internals.setValidity({ customError: true }, t3[0], this.fileInput) : this.internals.setValidity({});
@@ -5188,6 +5188,17 @@ var c3 = proxyCustomElement(class extends H {
5188
5188
  reset() {
5189
5189
  this.files = [];
5190
5190
  }
5191
+ formStateRestoreCallback(t2) {
5192
+ this.restorable(t2) ? this.value = t2 : this.updateFormValue();
5193
+ }
5194
+ restorable(t2) {
5195
+ if ("string" != typeof t2) return false;
5196
+ try {
5197
+ return Array.isArray(JSON.parse(t2));
5198
+ } catch {
5199
+ return false;
5200
+ }
5201
+ }
5191
5202
  handleFileInputChange = () => {
5192
5203
  this.fileInput?.files?.length && (this.addFiles(this.fileInput.files), this.fileInput.value = null);
5193
5204
  };
@@ -5215,7 +5226,7 @@ var c3 = proxyCustomElement(class extends H {
5215
5226
  return this.disabled || !!this.el.closest("fieldset[disabled]");
5216
5227
  }
5217
5228
  render() {
5218
- return h(Host, { key: "09588ce0af3411cc579d7d828c604fcc1fd44825" }, h("input", { key: "1311e26122c5778efc9eb1176c4a4986f1865d7d", ref: (t2) => this.fileInput = t2, type: "file", "aria-label": "Choose " + (this.multiple ? "files" : "file"), multiple: this.multiple, accept: this.accepts, required: this.required && 0 === this.files.length, disabled: this.isDisabled, onChange: () => this.handleFileInputChange() }), h("file-drop", { key: "7adac8601feb97f1123482bde83cf0bb7bd931b7", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "38faec2ec357d905719948b3f0f46b23abde4c40", part: "title" }, h("strong", { key: "61bd69a635852191f31e4e986f13bbf63e57dfa3" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "6ac8e031e03ed7d23c83edbacde03c441c4b4df7" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "476bdeafcf7e6211a3feccdc0c275efaf1bff928", class: "media-preview " + (this.multiple ? "-stacked" : "") }, h("slot", { key: "f843426c3a3cef23ccf8d3f1f584075762ecc8b2" }))));
5229
+ return h(Host, { key: "9b1ce57edcd31be2885c5923e65c8f4d0ac31002" }, h("input", { key: "a498cdd103d4b3385bfbfb4c2158ae2c15ed3b55", ref: (t2) => this.fileInput = t2, type: "file", "aria-label": "Choose " + (this.multiple ? "files" : "file"), multiple: this.multiple, accept: this.accepts, required: this.required && 0 === this.files.length, disabled: this.isDisabled, onChange: () => this.handleFileInputChange() }), h("file-drop", { key: "9edc0c1e22d0a6385515f6dff44965c9f3e7a40c", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "ae06d2f6be38f25eb977e66b1c6c565ca0e3c97d", part: "title" }, h("strong", { key: "c53fc410ec761ca7f044c25cceb1be12c6e37b5c" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "227a48d7ed21443ec97f646b58611cbf611145bc" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "25fce75fb15b66bd13bea450054d22ddabd05824", class: "media-preview " + (this.multiple ? "-stacked" : "") }, h("slot", { key: "86f6db9bef3f2d6c16a72679ec33b751aef4eec5" }))));
5219
5230
  }
5220
5231
  componentDidRender() {
5221
5232
  if (0 === this.files.length) {
@@ -3,5 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 8.1.0"
6
+ gem "json", "< 3"
6
7
 
7
8
  gemspec path: "../"
@@ -34,4 +34,45 @@ describe('input-attachment', () => {
34
34
  </input-attachment>
35
35
  `);
36
36
  });
37
+
38
+ describe('formStateRestoreCallback (Firefox session restore)', () => {
39
+ async function newPage() {
40
+ const page = await newSpecPage({
41
+ components: [InputAttachment],
42
+ html: `<form><input-attachment name="attachments[]" multiple></input-attachment></form>`,
43
+ });
44
+ const instance: any = page.rootInstance;
45
+ const calls: any[] = [];
46
+ instance.internals = {
47
+ setFormValue: (value: any, state: any) => calls.push({ value, state }),
48
+ setValidity: () => {},
49
+ };
50
+ return { page, instance, calls };
51
+ }
52
+
53
+ it('saves the files as a JSON string state, not the FormData Firefox would flatten', async () => {
54
+ const { instance, calls } = await newPage();
55
+ instance.files = [{ value: 'signed-one' }, { value: 'signed-two' }];
56
+ instance.updateFormValue();
57
+ const last = calls[calls.length - 1];
58
+ expect(typeof last.state).toBe('string');
59
+ expect(JSON.parse(last.state).map((f: any) => f.value)).toEqual(['signed-one', 'signed-two']);
60
+ });
61
+
62
+ it('rebuilds the attachments from the restored state string', async () => {
63
+ const { instance, calls } = await newPage();
64
+ const state = JSON.stringify([{ value: 'signed-one', filename: 'a.png', state: 'complete' }]);
65
+ instance.formStateRestoreCallback(state);
66
+ expect(instance.files.map((f: any) => f.value)).toEqual(['signed-one']);
67
+ instance.updateFormValue();
68
+ expect(calls[calls.length - 1].value.getAll('attachments[]')).toEqual(['signed-one']);
69
+ });
70
+
71
+ it('falls back to the current files when the restored state is the coerced "[object Object]"', async () => {
72
+ const { instance, calls } = await newPage();
73
+ instance.files = [{ value: 'signed-one' }];
74
+ instance.formStateRestoreCallback('[object Object]');
75
+ expect(calls[calls.length - 1].value.getAll('attachments[]')).toEqual(['signed-one']);
76
+ });
77
+ });
37
78
  });
@@ -125,7 +125,9 @@ export class InputAttachment {
125
125
  // For has_one_attached: set single value (or empty string if none)
126
126
  formData.set(this.name, values[0] || '')
127
127
  }
128
- this.internals.setFormValue(formData)
128
+ // State is a JSON string (this.value), not the FormData: Firefox flattens a FormData to {}
129
+ // in the session store and restores it as "[object Object]", but a string survives intact.
130
+ this.internals.setFormValue(formData, this.value)
129
131
 
130
132
  // Update validity state - check for required and child validation errors
131
133
  if (this.required && this.files.length === 0) {
@@ -147,6 +149,27 @@ export class InputAttachment {
147
149
  this.files = []
148
150
  }
149
151
 
152
+ // On session restore Firefox hands back the state we saved (this.value, a JSON string of the
153
+ // files). Rebuild the attachments from it so they survive a browser restart; fall back to
154
+ // re-deriving from the current files if the state is missing or malformed (e.g. the coerced
155
+ // "[object Object]", which is not valid JSON).
156
+ formStateRestoreCallback(state) {
157
+ if (this.restorable(state)) {
158
+ this.value = state
159
+ } else {
160
+ this.updateFormValue()
161
+ }
162
+ }
163
+
164
+ restorable(state): boolean {
165
+ if (typeof state !== "string") return false
166
+ try {
167
+ return Array.isArray(JSON.parse(state))
168
+ } catch {
169
+ return false
170
+ }
171
+ }
172
+
150
173
  handleFileInputChange = () => {
151
174
  if (!this.fileInput?.files?.length) return
152
175
  this.addFiles(this.fileInput.files)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module AttachmentField
5
- VERSION = "0.6.3"
5
+ VERSION = "0.6.4"
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.6.3
4
+ version: 0.6.4
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-08-13 00:00:00.000000000 Z
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage