Skip to content

Commit e47d0e0

Browse files
authored
Merge pull request #3526 from AlchemyCMS/fix-overlay-uploader
Fix overlay uploader
2 parents 50bba1c + d6b9c32 commit e47d0e0

File tree

8 files changed

+189
-28
lines changed

8 files changed

+189
-28
lines changed

app/javascript/alchemy_admin/components/uploader.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@ export class Uploader extends AlchemyHTMLElement {
2121
if (this.dropzone) {
2222
this._dragAndDropBehavior()
2323
}
24+
this.addEventListener("Alchemy.upload.successful", this)
25+
}
26+
27+
handleEvent(evt) {
28+
switch (evt.type) {
29+
case "Alchemy.upload.successful":
30+
this._handleUploadComplete()
31+
break
32+
}
33+
}
34+
35+
_handleUploadComplete() {
36+
setTimeout(() => {
37+
const url = this.redirectUrl
38+
const turboFrame = this.closest("turbo-frame")
39+
this.uploadProgress.visible = false
40+
41+
if (!url) return
42+
43+
if (turboFrame) {
44+
turboFrame.setAttribute("src", url)
45+
turboFrame.reload()
46+
} else {
47+
Turbo.visit(url)
48+
}
49+
}, 750)
2450
}
2551

2652
/**
@@ -126,6 +152,10 @@ export class Uploader extends AlchemyHTMLElement {
126152
get fileInput() {
127153
return this.querySelector("input[type='file']")
128154
}
155+
156+
get redirectUrl() {
157+
return this.getAttribute("redirect-url")
158+
}
129159
}
130160

131161
customElements.define("alchemy-uploader", Uploader)

app/views/alchemy/admin/attachments/_archive_overlay.html.erb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@
88
dropzone: '#assign_file_list',
99
file_attribute: 'file',
1010
in_dialog: true,
11+
accept: Array(search_filter_params[:only]).map { |extension|
12+
Marcel::MimeType.for(extension:)
13+
}.join(",").presence,
1114
redirect_url: admin_attachments_path(
12-
form_field_id: @form_field_id
15+
form_field_id: @form_field_id,
16+
only: search_filter_params[:only],
17+
except: search_filter_params[:except],
18+
q: search_filter_params[:q].merge(
19+
last_upload: true
20+
)
1321
) %>
1422
</div>
1523
<% end %>
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
<% file_upload_id = "file_upload_#{dom_id(object)}" %>
1+
<% file_types = Alchemy.config.uploader.allowed_filetypes[object.class.model_name.collection] || ['*'] %>
2+
<% accept ||= file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %>
23

3-
<alchemy-uploader id="<%= file_upload_id %>">
4+
<alchemy-uploader redirect-url="<%= redirect_url %>">
45
<%= form_for [:admin, object], html: {multipart: true, class: 'upload-button'} do |f| %>
56
<%= f.file_field file_attribute,
6-
class: 'fileupload--field',
7+
class: 'fileupload--field', accept: accept,
78
name: "#{f.object_name}[#{file_attribute}]",
89
id: "replace_#{dom_id(object)}" %>
910
<%= label_tag "replace_#{dom_id(object)}", class: "icon_button" do %>
1011
<%= render_icon "file-upload" %>
1112
<% end %>
1213
<% end %>
1314
</alchemy-uploader>
14-
15-
<script type="text/javascript">
16-
document.getElementById("<%= file_upload_id %>").addEventListener("Alchemy.upload.successful", (event) => {
17-
Turbo.visit('<%= redirect_url.html_safe %>');
18-
})
19-
</script>

app/views/alchemy/admin/attachments/index.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
<% if can? :create, Alchemy::Attachment %>
44
<div class="toolbar_button">
55
<%= render 'alchemy/admin/uploader/button',
6-
redirect_url: alchemy.admin_attachments_path,
6+
redirect_url: alchemy.admin_attachments_path(q: {
7+
last_upload: true
8+
}),
79
object: Alchemy::Attachment.new,
810
file_attribute: 'file' %>
911
</div>
Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<% file_types = Alchemy.config.uploader.allowed_filetypes[object.class.model_name.collection] || ['*'] %>
2-
<% accept = file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %>
2+
<% accept ||= file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %>
33

4-
<alchemy-uploader dropzone="<%= local_assigns[:dropzone] || "#main_content" %>">
4+
<alchemy-uploader redirect-url="<%= redirect_url %>" dropzone="<%= local_assigns[:dropzone] || "#main_content" %>">
55
<%= form_for [:admin, object], html: { multipart: true, class: 'upload-button' } do |f| %>
66
<%= f.file_field file_attribute,
77
class: 'fileupload fileupload--field', multiple: true, accept: accept,
@@ -15,17 +15,3 @@
1515
<% end %>
1616
<% end %>
1717
</alchemy-uploader>
18-
19-
<script type="text/javascript">
20-
document.querySelector("alchemy-uploader").addEventListener("Alchemy.upload.successful", (evt) => {
21-
setTimeout(() => {
22-
var url = '<%= redirect_url.html_safe %>';
23-
evt.target.uploadProgress.visible = false;
24-
<% if local_assigns[:in_dialog] %>
25-
$.get(url, null, null, 'script');
26-
<% else %>
27-
Turbo.visit(url);
28-
<% end %>
29-
}, 1000)
30-
})
31-
</script>

spec/javascript/alchemy_admin/components/uploader.spec.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,89 @@ describe("alchemy-uploader", () => {
282282
})
283283
})
284284
})
285+
286+
describe("_handleUploadComplete", () => {
287+
beforeEach(() => {
288+
vi.useFakeTimers()
289+
component._uploadFiles([firstFile])
290+
})
291+
292+
afterEach(() => {
293+
vi.useRealTimers()
294+
})
295+
296+
describe("without redirect-url", () => {
297+
beforeEach(() => {
298+
global.Turbo = { visit: vi.fn() }
299+
component.dispatchEvent(
300+
new CustomEvent("Alchemy.upload.successful", { bubbles: true })
301+
)
302+
})
303+
304+
it("hides the progress after timeout", () => {
305+
vi.advanceTimersByTime(750)
306+
expect(component.uploadProgress.visible).toBeFalsy()
307+
})
308+
309+
it("does not call Turbo.visit", () => {
310+
vi.advanceTimersByTime(750)
311+
expect(Turbo.visit).not.toHaveBeenCalled()
312+
})
313+
})
314+
315+
describe("without turbo-frame", () => {
316+
beforeEach(() => {
317+
global.Turbo = { visit: vi.fn() }
318+
component.setAttribute("redirect-url", "/admin/pictures")
319+
component.dispatchEvent(
320+
new CustomEvent("Alchemy.upload.successful", { bubbles: true })
321+
)
322+
})
323+
324+
it("hides the progress after timeout", () => {
325+
expect(component.uploadProgress.visible).toBeTruthy()
326+
vi.advanceTimersByTime(750)
327+
expect(component.uploadProgress.visible).toBeFalsy()
328+
})
329+
330+
it("visits the redirect URL via Turbo", () => {
331+
vi.advanceTimersByTime(750)
332+
expect(Turbo.visit).toHaveBeenCalledWith("/admin/pictures")
333+
})
334+
})
335+
336+
describe("with turbo-frame", () => {
337+
let turboFrame
338+
339+
beforeEach(() => {
340+
// Wrap the component in a turbo-frame
341+
turboFrame = document.createElement("turbo-frame")
342+
turboFrame.id = "test-frame"
343+
turboFrame.reload = vi.fn()
344+
component.parentNode.insertBefore(turboFrame, component)
345+
turboFrame.appendChild(component)
346+
347+
component.setAttribute("redirect-url", "/admin/pictures")
348+
component.dispatchEvent(
349+
new CustomEvent("Alchemy.upload.successful", { bubbles: true })
350+
)
351+
})
352+
353+
it("sets the turbo-frame src attribute", () => {
354+
vi.advanceTimersByTime(750)
355+
expect(turboFrame.getAttribute("src")).toBe("/admin/pictures")
356+
})
357+
358+
it("reloads the turbo-frame", () => {
359+
vi.advanceTimersByTime(750)
360+
expect(turboFrame.reload).toHaveBeenCalled()
361+
})
362+
363+
it("hides the progress after timeout", () => {
364+
expect(component.uploadProgress.visible).toBeTruthy()
365+
vi.advanceTimersByTime(750)
366+
expect(component.uploadProgress.visible).toBeFalsy()
367+
})
368+
})
369+
})
285370
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
describe "alchemy/admin/attachments/_replace_button.html.erb" do
6+
let(:object) { Alchemy::Attachment.new }
7+
let(:file_attribute) { :file }
8+
let(:redirect_url) { "/admin/attachments" }
9+
10+
before do
11+
allow(view).to receive(:admin_attachments_path).and_return("/admin/attachments")
12+
view.extend Alchemy::BaseHelper
13+
end
14+
15+
it "renders a alchemy-uploader component" do
16+
render partial: "alchemy/admin/attachments/replace_button",
17+
locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url}
18+
expect(rendered).to have_selector("alchemy-uploader[redirect-url='/admin/attachments']")
19+
end
20+
21+
context "with allowed_filetypes configured as wildcard" do
22+
before do
23+
allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_attachments) do
24+
["*"]
25+
end
26+
end
27+
28+
it "does not render the accept attribute" do
29+
render partial: "alchemy/admin/attachments/replace_button",
30+
locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url}
31+
32+
expect(rendered).not_to have_selector('input[type="file"][accept]')
33+
end
34+
end
35+
36+
context "with allowed_filetypes configured as specific file types" do
37+
before do
38+
allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_attachments) do
39+
["pdf", "doc", "docx"]
40+
end
41+
end
42+
43+
it "renders the accept attribute with the correct file extensions" do
44+
render partial: "alchemy/admin/attachments/replace_button",
45+
locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url}
46+
47+
expect(rendered).to have_selector('input[type="file"][accept=".pdf, .doc, .docx"]')
48+
end
49+
end
50+
end

spec/views/alchemy/admin/uploader/_button.html.erb_spec.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
let(:redirect_url) { "/admin/pictures" }
99

1010
before do
11-
allow(view).to receive(:can?).and_return(true)
1211
allow(view).to receive(:admin_pictures_path).and_return("/admin/pictures")
1312
allow(view).to receive(:admin_attachments_path).and_return("/admin/attachments")
1413
view.extend Alchemy::BaseHelper
1514
end
1615

16+
it "renders a alchemy-uploader component" do
17+
render partial: "alchemy/admin/uploader/button",
18+
locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url}
19+
expect(rendered).to have_selector("alchemy-uploader[redirect-url='/admin/pictures']")
20+
end
21+
1722
context "when wildcard is configured (all file types allowed)" do
1823
before do
1924
allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_pictures) do

0 commit comments

Comments
 (0)