bard-attachment_field 0.2.0 → 0.2.1
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/app/assets/javascripts/input-attachment.js +69 -22
- data/input-attachment/bun.lockb +0 -0
- data/input-attachment/package.json +1 -0
- data/input-attachment/src/components/input-attachment/form-controller.tsx +1 -0
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +7 -6
- data/lib/bard/attachment_field/cucumber.rb +69 -17
- data/lib/bard/attachment_field/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b56542b6ac4750d33522c0dc996d1ae7f1bb772c29d455a04202f8c2a980a5b2
|
|
4
|
+
data.tar.gz: c114839ed9845fe0dd49f72498964bc6f5f88a8fee27c96ba9a77366087bfc84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15d3778a7d97bea1e1433e42ceee7df8c31be1b2d1cd2879de577d5468592ead3fe142822a28de716870493bc9bfb869f21dc0ddfc99faa545b62f7a6b9e6323
|
|
7
|
+
data.tar.gz: 8d304feeaf8a31f2ef5b3731c58f9e683be9bd9a42039b40dd95c7f573cf969878fd35e8b1b17388b415fca6ccf737c0fe2a1bd9ac6bf2eca2ad6db3b9f542e5
|
|
@@ -5070,6 +5070,9 @@ var FetchResponse = class {
|
|
|
5070
5070
|
get isTurboStream() {
|
|
5071
5071
|
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/);
|
|
5072
5072
|
}
|
|
5073
|
+
get isScript() {
|
|
5074
|
+
return this.contentType.match(/\b(?:java|ecma)script\b/);
|
|
5075
|
+
}
|
|
5073
5076
|
async renderTurboStream() {
|
|
5074
5077
|
if (this.isTurboStream) {
|
|
5075
5078
|
if (window.Turbo) {
|
|
@@ -5081,6 +5084,22 @@ var FetchResponse = class {
|
|
|
5081
5084
|
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
|
|
5082
5085
|
}
|
|
5083
5086
|
}
|
|
5087
|
+
async activeScript() {
|
|
5088
|
+
if (this.isScript) {
|
|
5089
|
+
const script = document.createElement("script");
|
|
5090
|
+
const metaTag = document.querySelector("meta[name=csp-nonce]");
|
|
5091
|
+
if (metaTag) {
|
|
5092
|
+
const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
|
|
5093
|
+
if (nonce) {
|
|
5094
|
+
script.setAttribute("nonce", nonce);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
script.innerHTML = await this.text;
|
|
5098
|
+
document.body.appendChild(script);
|
|
5099
|
+
} else {
|
|
5100
|
+
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5084
5103
|
};
|
|
5085
5104
|
var RequestInterceptor = class {
|
|
5086
5105
|
static register(interceptor) {
|
|
@@ -5126,7 +5145,7 @@ function stringEntriesFromFormData(formData) {
|
|
|
5126
5145
|
function mergeEntries(searchParams, entries) {
|
|
5127
5146
|
for (const [name, value] of entries) {
|
|
5128
5147
|
if (value instanceof window.File) continue;
|
|
5129
|
-
if (searchParams.has(name)) {
|
|
5148
|
+
if (searchParams.has(name) && !name.includes("[]")) {
|
|
5130
5149
|
searchParams.delete(name);
|
|
5131
5150
|
searchParams.set(name, value);
|
|
5132
5151
|
} else {
|
|
@@ -5149,11 +5168,16 @@ var FetchRequest = class {
|
|
|
5149
5168
|
} catch (error) {
|
|
5150
5169
|
console.error(error);
|
|
5151
5170
|
}
|
|
5152
|
-
const
|
|
5171
|
+
const fetch = window.Turbo ? window.Turbo.fetch : window.fetch;
|
|
5172
|
+
const response = new FetchResponse(await fetch(this.url, this.fetchOptions));
|
|
5153
5173
|
if (response.unauthenticated && response.authenticationURL) {
|
|
5154
5174
|
return Promise.reject(window.location.href = response.authenticationURL);
|
|
5155
5175
|
}
|
|
5156
|
-
if (response.
|
|
5176
|
+
if (response.isScript) {
|
|
5177
|
+
await response.activeScript();
|
|
5178
|
+
}
|
|
5179
|
+
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
|
|
5180
|
+
if (responseStatusIsTurboStreamable && response.isTurboStream) {
|
|
5157
5181
|
await response.renderTurboStream();
|
|
5158
5182
|
}
|
|
5159
5183
|
return response;
|
|
@@ -5163,27 +5187,38 @@ var FetchRequest = class {
|
|
|
5163
5187
|
headers[key] = value;
|
|
5164
5188
|
this.options.headers = headers;
|
|
5165
5189
|
}
|
|
5190
|
+
sameHostname() {
|
|
5191
|
+
if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
|
|
5192
|
+
return true;
|
|
5193
|
+
}
|
|
5194
|
+
try {
|
|
5195
|
+
return new URL(this.originalUrl).hostname === window.location.hostname;
|
|
5196
|
+
} catch (_) {
|
|
5197
|
+
return true;
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5166
5200
|
get fetchOptions() {
|
|
5167
5201
|
return {
|
|
5168
5202
|
method: this.method.toUpperCase(),
|
|
5169
5203
|
headers: this.headers,
|
|
5170
5204
|
body: this.formattedBody,
|
|
5171
5205
|
signal: this.signal,
|
|
5172
|
-
credentials:
|
|
5173
|
-
redirect: this.redirect
|
|
5206
|
+
credentials: this.credentials,
|
|
5207
|
+
redirect: this.redirect,
|
|
5208
|
+
keepalive: this.keepalive
|
|
5174
5209
|
};
|
|
5175
5210
|
}
|
|
5176
5211
|
get headers() {
|
|
5212
|
+
const baseHeaders = {
|
|
5213
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
5214
|
+
"Content-Type": this.contentType,
|
|
5215
|
+
Accept: this.accept
|
|
5216
|
+
};
|
|
5217
|
+
if (this.sameHostname()) {
|
|
5218
|
+
baseHeaders["X-CSRF-Token"] = this.csrfToken;
|
|
5219
|
+
}
|
|
5177
5220
|
return compact(
|
|
5178
|
-
Object.assign(
|
|
5179
|
-
{
|
|
5180
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
5181
|
-
"X-CSRF-Token": this.csrfToken,
|
|
5182
|
-
"Content-Type": this.contentType,
|
|
5183
|
-
Accept: this.accept
|
|
5184
|
-
},
|
|
5185
|
-
this.additionalHeaders
|
|
5186
|
-
)
|
|
5221
|
+
Object.assign(baseHeaders, this.additionalHeaders)
|
|
5187
5222
|
);
|
|
5188
5223
|
}
|
|
5189
5224
|
get csrfToken() {
|
|
@@ -5207,6 +5242,8 @@ var FetchRequest = class {
|
|
|
5207
5242
|
return "text/vnd.turbo-stream.html, text/html, application/xhtml+xml";
|
|
5208
5243
|
case "json":
|
|
5209
5244
|
return "application/json, application/vnd.api+json";
|
|
5245
|
+
case "script":
|
|
5246
|
+
return "text/javascript, application/javascript";
|
|
5210
5247
|
default:
|
|
5211
5248
|
return "*/*";
|
|
5212
5249
|
}
|
|
@@ -5241,6 +5278,12 @@ var FetchRequest = class {
|
|
|
5241
5278
|
get redirect() {
|
|
5242
5279
|
return this.options.redirect || "follow";
|
|
5243
5280
|
}
|
|
5281
|
+
get credentials() {
|
|
5282
|
+
return this.options.credentials || "same-origin";
|
|
5283
|
+
}
|
|
5284
|
+
get keepalive() {
|
|
5285
|
+
return this.options.keepalive || false;
|
|
5286
|
+
}
|
|
5244
5287
|
get additionalHeaders() {
|
|
5245
5288
|
return this.options.headers || {};
|
|
5246
5289
|
}
|
|
@@ -5256,7 +5299,7 @@ var FetchRequest = class {
|
|
|
5256
5299
|
var request = (verb, url, payload, headers) => {
|
|
5257
5300
|
const req = new FetchRequest(verb, url, {
|
|
5258
5301
|
headers: { Accept: "application/json", ...headers },
|
|
5259
|
-
|
|
5302
|
+
...{ query: payload }
|
|
5260
5303
|
});
|
|
5261
5304
|
return req.perform().then((response) => {
|
|
5262
5305
|
if (response.response.ok) {
|
|
@@ -5466,6 +5509,8 @@ var FormController = class _FormController {
|
|
|
5466
5509
|
}
|
|
5467
5510
|
}
|
|
5468
5511
|
submit(event) {
|
|
5512
|
+
if (this.controllers.length === 0 && !this.hasUploadErrors())
|
|
5513
|
+
return;
|
|
5469
5514
|
event.preventDefault();
|
|
5470
5515
|
this.submitted = true;
|
|
5471
5516
|
this.startNextController();
|
|
@@ -5814,11 +5859,13 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5814
5859
|
set value(val) {
|
|
5815
5860
|
const newValue = val || [];
|
|
5816
5861
|
if (JSON.stringify(this.value) !== JSON.stringify(newValue)) {
|
|
5817
|
-
this.files = newValue.map((signedId) =>
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5862
|
+
this.files = newValue.map((signedId) => {
|
|
5863
|
+
const attachmentFile = document.createElement("attachment-file");
|
|
5864
|
+
attachmentFile.name = this.name;
|
|
5865
|
+
attachmentFile.preview = this.preview;
|
|
5866
|
+
attachmentFile.signedId = signedId;
|
|
5867
|
+
return attachmentFile;
|
|
5868
|
+
});
|
|
5822
5869
|
}
|
|
5823
5870
|
}
|
|
5824
5871
|
// For form-persistence: store complete attachment data (not just signed_ids)
|
|
@@ -5912,12 +5959,12 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5912
5959
|
return this.disabled || !!this.el.closest("fieldset[disabled]");
|
|
5913
5960
|
}
|
|
5914
5961
|
render() {
|
|
5915
|
-
return h(Host, { key: "
|
|
5962
|
+
return h(Host, { key: "4ef61015f6e0abb87d50a3f9ff8e39f01dc44ead" }, h("input", { key: "fc02ff04ff705c2c26ef6bc9e21e23435eefe0d6", ref: (el) => this.fileInput = el, type: "file", multiple: this.multiple, accept: this.accepts, required: this.required && this.files.length === 0, disabled: this.isDisabled, onChange: () => this.handleFileInputChange(), style: {
|
|
5916
5963
|
opacity: "0.01",
|
|
5917
5964
|
width: "1px",
|
|
5918
5965
|
height: "1px",
|
|
5919
5966
|
zIndex: "-999"
|
|
5920
|
-
} }), h("file-drop", { key: "
|
|
5967
|
+
} }), h("file-drop", { key: "20ad744ab366a7af2404711a025240515198815f", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "c5289e9d5b03ced01d95f2d2152ab06dfaaaa8fb", part: "title" }, h("strong", { key: "ed48073458e73fad729caebc1eea8c1ed31fd021" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "daa4fbd6f33180060a306fdb5ea469d2a68a141b" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "83a5aaf4ee4bc0d7c2917a781ef77568ce9fb9a6", class: `media-preview ${this.multiple ? "-stacked" : ""}` }, h("slot", { key: "8ce39f9805a8f292cc08c9d77c8f91d2754152f7" }))));
|
|
5921
5968
|
}
|
|
5922
5969
|
componentDidRender() {
|
|
5923
5970
|
if (this.files.length === 0) {
|
data/input-attachment/bun.lockb
CHANGED
|
Binary file
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Component, Element, Prop, Listen, Host, h, forceUpdate } from '@stencil/core';
|
|
2
2
|
import FormController from "./form-controller"
|
|
3
|
-
import { AttachmentFile } from "../attachment-file/attachment-file"
|
|
4
3
|
import { arrayRemove } from "../../utils/utils"
|
|
5
4
|
import '@botandrose/file-drop'
|
|
6
5
|
import '@botandrose/progress-bar'
|
|
@@ -125,11 +124,13 @@ export class InputAttachment {
|
|
|
125
124
|
set value(val) {
|
|
126
125
|
const newValue = val || []
|
|
127
126
|
if(JSON.stringify(this.value) !== JSON.stringify(newValue)) { // this is insane. javascript is fucking garbage.
|
|
128
|
-
this.files = newValue.map(signedId =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
this.files = newValue.map(signedId => {
|
|
128
|
+
const attachmentFile = document.createElement('attachment-file') as any
|
|
129
|
+
attachmentFile.name = this.name
|
|
130
|
+
attachmentFile.preview = this.preview
|
|
131
|
+
attachmentFile.signedId = signedId
|
|
132
|
+
return attachmentFile
|
|
133
|
+
})
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -24,7 +24,11 @@ module Bard::AttachmentField::TestHelper
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def find_field(session, label)
|
|
27
|
-
label_element =
|
|
27
|
+
label_element = begin
|
|
28
|
+
session.find("label", exact_text: label, visible: :all, match: :first)
|
|
29
|
+
rescue Capybara::ElementNotFound
|
|
30
|
+
session.find("label", text: /^#{Regexp.escape(label)}/, visible: :all, match: :first)
|
|
31
|
+
end
|
|
28
32
|
element_id = label_element[:for]
|
|
29
33
|
session.find("input-attachment##{element_id}")
|
|
30
34
|
end
|
|
@@ -34,7 +38,7 @@ module Bard::AttachmentField::TestHelper
|
|
|
34
38
|
def attach_files(session, element_id, file_paths)
|
|
35
39
|
session.execute_script("document.body.insertAdjacentHTML('beforeend', '<input type=\"file\" id=\"_cdp_file_helper\" multiple style=\"display:none\">')")
|
|
36
40
|
|
|
37
|
-
temp_input = session.find("#_cdp_file_helper", visible: :all)
|
|
41
|
+
temp_input = session.document.find("#_cdp_file_helper", visible: :all)
|
|
38
42
|
temp_input.native.node.select_file(file_paths)
|
|
39
43
|
|
|
40
44
|
session.execute_script(<<~JS)
|
|
@@ -47,12 +51,32 @@ module Bard::AttachmentField::TestHelper
|
|
|
47
51
|
JS
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
def wait_for_files(session, element_id, minimum, timeout: 15)
|
|
55
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
56
|
+
loop do
|
|
57
|
+
count = session.evaluate_script("document.getElementById('#{element_id}').querySelectorAll('attachment-file').length")
|
|
58
|
+
break if count >= minimum
|
|
59
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
60
|
+
raise "Expected at least #{minimum} attachment-file(s) in ##{element_id}, found #{count} after #{timeout}s" if elapsed > timeout
|
|
61
|
+
sleep 0.1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
50
65
|
def wait_for_upload(session, element_id, timeout: 30)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
loop do
|
|
68
|
+
result = session.evaluate_script(<<~JS)
|
|
69
|
+
(() => {
|
|
70
|
+
const files = document.getElementById('#{element_id}').querySelectorAll('attachment-file');
|
|
71
|
+
return Array.from(files).map(e => ({ state: e.getAttribute('state'), value: e.value }));
|
|
72
|
+
})()
|
|
54
73
|
JS
|
|
55
|
-
|
|
74
|
+
states_done = result.all? { |f| f["state"] == "complete" || f["state"] == "error" }
|
|
75
|
+
values_set = result.all? { |f| f["state"] == "error" || (f["value"] && !f["value"].empty?) }
|
|
76
|
+
break if states_done && values_set
|
|
77
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
78
|
+
raise "Uploads not complete after #{timeout}s (files=#{result})" if elapsed > timeout
|
|
79
|
+
sleep 0.1
|
|
56
80
|
end
|
|
57
81
|
end
|
|
58
82
|
|
|
@@ -116,6 +140,7 @@ class Chop::Form::AttachmentField < Chop::Form::Field
|
|
|
116
140
|
end
|
|
117
141
|
|
|
118
142
|
Bard::AttachmentField::TestHelper.attach_files(session, field[:id], file_paths)
|
|
143
|
+
Bard::AttachmentField::TestHelper.wait_for_files(session, field[:id], file_paths.length)
|
|
119
144
|
Bard::AttachmentField::TestHelper.wait_for_upload(session, field[:id])
|
|
120
145
|
end
|
|
121
146
|
end
|
|
@@ -123,29 +148,32 @@ end
|
|
|
123
148
|
# Step definitions
|
|
124
149
|
|
|
125
150
|
When "I attach the file {string} to {string}" do |path, field|
|
|
126
|
-
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
127
151
|
file_path_full = Bard::AttachmentField::TestHelper.resolve_fixture_path(path)
|
|
128
152
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
begin
|
|
154
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
155
|
+
rescue Capybara::ElementNotFound
|
|
156
|
+
attach_file field, file_path_full
|
|
157
|
+
next
|
|
133
158
|
end
|
|
134
159
|
|
|
160
|
+
Bard::AttachmentField::TestHelper.attach_files(page, element[:id], [file_path_full])
|
|
161
|
+
Bard::AttachmentField::TestHelper.wait_for_files(page, element[:id], 1)
|
|
135
162
|
Bard::AttachmentField::TestHelper.wait_for_upload(page, element[:id])
|
|
136
163
|
end
|
|
137
164
|
|
|
138
165
|
When "I attach the following files to {string}:" do |field, table|
|
|
139
166
|
files = table.raw.map(&:first).map { |filename| Bard::AttachmentField::TestHelper.resolve_fixture_path(filename) }
|
|
140
167
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
element.all("attachment-file", minimum: files.length)
|
|
168
|
+
begin
|
|
169
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
170
|
+
rescue Capybara::ElementNotFound
|
|
171
|
+
attach_file field, files
|
|
172
|
+
next
|
|
147
173
|
end
|
|
148
174
|
|
|
175
|
+
Bard::AttachmentField::TestHelper.attach_files(page, element[:id], files)
|
|
176
|
+
Bard::AttachmentField::TestHelper.wait_for_files(page, element[:id], files.length)
|
|
149
177
|
Bard::AttachmentField::TestHelper.wait_for_upload(page, element[:id], timeout: 60)
|
|
150
178
|
end
|
|
151
179
|
|
|
@@ -187,6 +215,23 @@ When "I remove {string} from {string}" do |filename, field|
|
|
|
187
215
|
end
|
|
188
216
|
end
|
|
189
217
|
|
|
218
|
+
When "I remove {string} from the {string} field" do |filename, field|
|
|
219
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
220
|
+
element_id = element[:id]
|
|
221
|
+
page.execute_script(<<~JS, element_id, filename)
|
|
222
|
+
((elementId, filename) => {
|
|
223
|
+
const host = document.getElementById(elementId);
|
|
224
|
+
const attachmentFile = host.querySelector(`attachment-file[filename='${filename}']`);
|
|
225
|
+
const removeLink = attachmentFile.shadowRoot.querySelector('a.remove-media');
|
|
226
|
+
removeLink.click();
|
|
227
|
+
})(arguments[0], arguments[1])
|
|
228
|
+
JS
|
|
229
|
+
page.document.synchronize(5, errors: [Capybara::ElementNotFound]) do
|
|
230
|
+
fresh_element = page.find("##{element_id}")
|
|
231
|
+
expect(fresh_element).to have_no_css("attachment-file[filename='#{filename}']")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
190
235
|
When "I drag the file {string} onto the {string} attachment field" do |path, field|
|
|
191
236
|
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
192
237
|
file_path_full = Bard::AttachmentField::TestHelper.resolve_fixture_path(path)
|
|
@@ -227,6 +272,13 @@ Then "I should see a preview of {string} for the {string} field" do |filename, f
|
|
|
227
272
|
expect(element.find("attachment-file")[:filename]).to eq(filename)
|
|
228
273
|
end
|
|
229
274
|
|
|
275
|
+
Then "I should see the following media previews for the {string} field:" do |field, table|
|
|
276
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
277
|
+
element.assert_selector("attachment-file", minimum: table.raw.length)
|
|
278
|
+
actual = Bard::AttachmentField::TestHelper.get_files(element).map { |f| [f] }
|
|
279
|
+
table.diff! actual
|
|
280
|
+
end
|
|
281
|
+
|
|
230
282
|
When "I follow the {string} download link for {string}" do |filename, field|
|
|
231
283
|
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
232
284
|
# Click the download link inside attachment-file's shadow DOM
|
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.2.
|
|
4
|
+
version: 0.2.1
|
|
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-03-
|
|
11
|
+
date: 2026-03-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activestorage
|