diff --git a/app/javascript/alchemy_admin/components/uploader.js b/app/javascript/alchemy_admin/components/uploader.js index 0b8bfb3c4d..470303dcb5 100644 --- a/app/javascript/alchemy_admin/components/uploader.js +++ b/app/javascript/alchemy_admin/components/uploader.js @@ -21,6 +21,32 @@ export class Uploader extends AlchemyHTMLElement { if (this.dropzone) { this._dragAndDropBehavior() } + this.addEventListener("Alchemy.upload.successful", this) + } + + handleEvent(evt) { + switch (evt.type) { + case "Alchemy.upload.successful": + this._handleUploadComplete() + break + } + } + + _handleUploadComplete() { + setTimeout(() => { + const url = this.redirectUrl + const turboFrame = this.closest("turbo-frame") + this.uploadProgress.visible = false + + if (!url) return + + if (turboFrame) { + turboFrame.setAttribute("src", url) + turboFrame.reload() + } else { + Turbo.visit(url) + } + }, 750) } /** @@ -126,6 +152,10 @@ export class Uploader extends AlchemyHTMLElement { get fileInput() { return this.querySelector("input[type='file']") } + + get redirectUrl() { + return this.getAttribute("redirect-url") + } } customElements.define("alchemy-uploader", Uploader) diff --git a/app/views/alchemy/admin/attachments/_archive_overlay.html.erb b/app/views/alchemy/admin/attachments/_archive_overlay.html.erb index 2715338e83..e1c813d0e2 100644 --- a/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +++ b/app/views/alchemy/admin/attachments/_archive_overlay.html.erb @@ -8,8 +8,16 @@ dropzone: '#assign_file_list', file_attribute: 'file', in_dialog: true, + accept: Array(search_filter_params[:only]).map { |extension| + Marcel::MimeType.for(extension:) + }.join(",").presence, redirect_url: admin_attachments_path( - form_field_id: @form_field_id + form_field_id: @form_field_id, + only: search_filter_params[:only], + except: search_filter_params[:except], + q: search_filter_params[:q].merge( + last_upload: true + ) ) %> <% end %> diff --git a/app/views/alchemy/admin/attachments/_replace_button.html.erb b/app/views/alchemy/admin/attachments/_replace_button.html.erb index 8f5aa723be..0646b72d85 100644 --- a/app/views/alchemy/admin/attachments/_replace_button.html.erb +++ b/app/views/alchemy/admin/attachments/_replace_button.html.erb @@ -1,9 +1,10 @@ -<% file_upload_id = "file_upload_#{dom_id(object)}" %> +<% file_types = Alchemy.config.uploader.allowed_filetypes[object.class.model_name.collection] || ['*'] %> +<% accept ||= file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %> - + <%= form_for [:admin, object], html: {multipart: true, class: 'upload-button'} do |f| %> <%= f.file_field file_attribute, - class: 'fileupload--field', + class: 'fileupload--field', accept: accept, name: "#{f.object_name}[#{file_attribute}]", id: "replace_#{dom_id(object)}" %> <%= label_tag "replace_#{dom_id(object)}", class: "icon_button" do %> @@ -11,9 +12,3 @@ <% end %> <% end %> - - diff --git a/app/views/alchemy/admin/attachments/index.html.erb b/app/views/alchemy/admin/attachments/index.html.erb index c87395fff8..b4699fed47 100644 --- a/app/views/alchemy/admin/attachments/index.html.erb +++ b/app/views/alchemy/admin/attachments/index.html.erb @@ -3,7 +3,9 @@ <% if can? :create, Alchemy::Attachment %>
<%= render 'alchemy/admin/uploader/button', - redirect_url: alchemy.admin_attachments_path, + redirect_url: alchemy.admin_attachments_path(q: { + last_upload: true + }), object: Alchemy::Attachment.new, file_attribute: 'file' %>
diff --git a/app/views/alchemy/admin/uploader/_button.html.erb b/app/views/alchemy/admin/uploader/_button.html.erb index 7fd2dc1a5f..12f84177fe 100644 --- a/app/views/alchemy/admin/uploader/_button.html.erb +++ b/app/views/alchemy/admin/uploader/_button.html.erb @@ -1,7 +1,7 @@ <% file_types = Alchemy.config.uploader.allowed_filetypes[object.class.model_name.collection] || ['*'] %> -<% accept = file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %> +<% accept ||= file_types.to_a == ["*"] ? nil : file_types.map {|type| ".#{type}"}.join(", ") %> -"> +"> <%= form_for [:admin, object], html: { multipart: true, class: 'upload-button' } do |f| %> <%= f.file_field file_attribute, class: 'fileupload fileupload--field', multiple: true, accept: accept, @@ -15,17 +15,3 @@ <% end %> <% end %> - - diff --git a/spec/javascript/alchemy_admin/components/uploader.spec.js b/spec/javascript/alchemy_admin/components/uploader.spec.js index 0f53941623..72a28753fd 100644 --- a/spec/javascript/alchemy_admin/components/uploader.spec.js +++ b/spec/javascript/alchemy_admin/components/uploader.spec.js @@ -282,4 +282,89 @@ describe("alchemy-uploader", () => { }) }) }) + + describe("_handleUploadComplete", () => { + beforeEach(() => { + vi.useFakeTimers() + component._uploadFiles([firstFile]) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe("without redirect-url", () => { + beforeEach(() => { + global.Turbo = { visit: vi.fn() } + component.dispatchEvent( + new CustomEvent("Alchemy.upload.successful", { bubbles: true }) + ) + }) + + it("hides the progress after timeout", () => { + vi.advanceTimersByTime(750) + expect(component.uploadProgress.visible).toBeFalsy() + }) + + it("does not call Turbo.visit", () => { + vi.advanceTimersByTime(750) + expect(Turbo.visit).not.toHaveBeenCalled() + }) + }) + + describe("without turbo-frame", () => { + beforeEach(() => { + global.Turbo = { visit: vi.fn() } + component.setAttribute("redirect-url", "/admin/pictures") + component.dispatchEvent( + new CustomEvent("Alchemy.upload.successful", { bubbles: true }) + ) + }) + + it("hides the progress after timeout", () => { + expect(component.uploadProgress.visible).toBeTruthy() + vi.advanceTimersByTime(750) + expect(component.uploadProgress.visible).toBeFalsy() + }) + + it("visits the redirect URL via Turbo", () => { + vi.advanceTimersByTime(750) + expect(Turbo.visit).toHaveBeenCalledWith("/admin/pictures") + }) + }) + + describe("with turbo-frame", () => { + let turboFrame + + beforeEach(() => { + // Wrap the component in a turbo-frame + turboFrame = document.createElement("turbo-frame") + turboFrame.id = "test-frame" + turboFrame.reload = vi.fn() + component.parentNode.insertBefore(turboFrame, component) + turboFrame.appendChild(component) + + component.setAttribute("redirect-url", "/admin/pictures") + component.dispatchEvent( + new CustomEvent("Alchemy.upload.successful", { bubbles: true }) + ) + }) + + it("sets the turbo-frame src attribute", () => { + vi.advanceTimersByTime(750) + expect(turboFrame.getAttribute("src")).toBe("/admin/pictures") + }) + + it("reloads the turbo-frame", () => { + vi.advanceTimersByTime(750) + expect(turboFrame.reload).toHaveBeenCalled() + }) + + it("hides the progress after timeout", () => { + expect(component.uploadProgress.visible).toBeTruthy() + vi.advanceTimersByTime(750) + expect(component.uploadProgress.visible).toBeFalsy() + }) + }) + }) }) diff --git a/spec/views/alchemy/admin/attachments/_replace_button.html.erb_spec.rb b/spec/views/alchemy/admin/attachments/_replace_button.html.erb_spec.rb new file mode 100644 index 0000000000..4bd496f89b --- /dev/null +++ b/spec/views/alchemy/admin/attachments/_replace_button.html.erb_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "alchemy/admin/attachments/_replace_button.html.erb" do + let(:object) { Alchemy::Attachment.new } + let(:file_attribute) { :file } + let(:redirect_url) { "/admin/attachments" } + + before do + allow(view).to receive(:admin_attachments_path).and_return("/admin/attachments") + view.extend Alchemy::BaseHelper + end + + it "renders a alchemy-uploader component" do + render partial: "alchemy/admin/attachments/replace_button", + locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url} + expect(rendered).to have_selector("alchemy-uploader[redirect-url='/admin/attachments']") + end + + context "with allowed_filetypes configured as wildcard" do + before do + allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_attachments) do + ["*"] + end + end + + it "does not render the accept attribute" do + render partial: "alchemy/admin/attachments/replace_button", + locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url} + + expect(rendered).not_to have_selector('input[type="file"][accept]') + end + end + + context "with allowed_filetypes configured as specific file types" do + before do + allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_attachments) do + ["pdf", "doc", "docx"] + end + end + + it "renders the accept attribute with the correct file extensions" do + render partial: "alchemy/admin/attachments/replace_button", + locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url} + + expect(rendered).to have_selector('input[type="file"][accept=".pdf, .doc, .docx"]') + end + end +end diff --git a/spec/views/alchemy/admin/uploader/_button.html.erb_spec.rb b/spec/views/alchemy/admin/uploader/_button.html.erb_spec.rb index 0899a8d07c..2f20c6d32b 100644 --- a/spec/views/alchemy/admin/uploader/_button.html.erb_spec.rb +++ b/spec/views/alchemy/admin/uploader/_button.html.erb_spec.rb @@ -8,12 +8,17 @@ let(:redirect_url) { "/admin/pictures" } before do - allow(view).to receive(:can?).and_return(true) allow(view).to receive(:admin_pictures_path).and_return("/admin/pictures") allow(view).to receive(:admin_attachments_path).and_return("/admin/attachments") view.extend Alchemy::BaseHelper end + it "renders a alchemy-uploader component" do + render partial: "alchemy/admin/uploader/button", + locals: {object: object, file_attribute: file_attribute, redirect_url: redirect_url} + expect(rendered).to have_selector("alchemy-uploader[redirect-url='/admin/pictures']") + end + context "when wildcard is configured (all file types allowed)" do before do allow(Alchemy.config.uploader.allowed_filetypes).to receive(:alchemy_pictures) do