diff --git a/admin/app/components/solidus_admin/return_reasons/edit/component.html.erb b/admin/app/components/solidus_admin/return_reasons/edit/component.html.erb new file mode 100644 index 00000000000..8782aa23c43 --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/edit/component.html.erb @@ -0,0 +1,26 @@ +<%= turbo_frame_tag :edit_return_reason_modal do %> + <%= render component("ui/modal").new(title: t(".title")) do |modal| %> + <%= form_for @return_reason, url: solidus_admin.return_reason_path(@return_reason), html: { id: form_id } do |f| %> +
+ <%= render component("ui/forms/field").text_field(f, :name, class: "required") %> + +
+ <% modal.with_actions do %> +
+ <%= render component("ui/button").new(scheme: :secondary, text: t('.cancel')) %> +
+ <%= render component("ui/button").new(form: form_id, type: :submit, text: t('.submit')) %> + <% end %> + <% end %> + <% end %> +<% end %> +<%= render component("return_reasons/index").new(page: @page) %> diff --git a/admin/app/components/solidus_admin/return_reasons/edit/component.rb b/admin/app/components/solidus_admin/return_reasons/edit/component.rb new file mode 100644 index 00000000000..2ce5ed7f0bf --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/edit/component.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SolidusAdmin::ReturnReasons::Edit::Component < SolidusAdmin::BaseComponent + def initialize(page:, return_reason:) + @page = page + @return_reason = return_reason + end + + def form_id + dom_id(@return_reason, "#{stimulus_id}_edit_return_reason_form") + end +end diff --git a/admin/app/components/solidus_admin/return_reasons/edit/component.yml b/admin/app/components/solidus_admin/return_reasons/edit/component.yml new file mode 100644 index 00000000000..03806d9bf30 --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/edit/component.yml @@ -0,0 +1,8 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + title: "Edit Return Reason" + cancel: "Cancel" + submit: "Update Return Reason" + hints: + active: "When checked, this return reason will be available for selection when returning orders." diff --git a/admin/app/components/solidus_admin/return_reasons/index/component.rb b/admin/app/components/solidus_admin/return_reasons/index/component.rb index 75ba35f2b95..175d9575828 100644 --- a/admin/app/components/solidus_admin/return_reasons/index/component.rb +++ b/admin/app/components/solidus_admin/return_reasons/index/component.rb @@ -14,7 +14,25 @@ def search_key end def row_url(return_reason) - spree.edit_admin_return_reason_path(return_reason) + spree.edit_admin_return_reason_path(return_reason, _turbo_frame: :edit_return_reason_modal) + end + + def turbo_frames + %w[ + new_return_reason_modal + edit_return_reason_modal + ] + end + + def page_actions + render component("ui/button").new( + tag: :a, + text: t('.add'), + href: solidus_admin.new_return_reason_path, + data: { turbo_frame: :new_return_reason_modal }, + icon: "add-line", + class: "align-self-end w-full", + ) end def batch_actions diff --git a/admin/app/components/solidus_admin/return_reasons/new/component.html.erb b/admin/app/components/solidus_admin/return_reasons/new/component.html.erb new file mode 100644 index 00000000000..7a55ebbcacd --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/new/component.html.erb @@ -0,0 +1,27 @@ +<%= turbo_frame_tag :new_return_reason_modal do %> + <%= render component("ui/modal").new(title: t(".title")) do |modal| %> + <%= form_for @return_reason, url: solidus_admin.return_reasons_path, html: { id: form_id } do |f| %> +
+ <%= render component("ui/forms/field").text_field(f, :name, class: "required") %> + +
+ <% modal.with_actions do %> +
+ <%= render component("ui/button").new(scheme: :secondary, text: t('.cancel')) %> +
+ <%= render component("ui/button").new(form: form_id, type: :submit, text: t('.submit')) %> + <% end %> + <% end %> + <% end %> +<% end %> + +<%= render component("return_reasons/index").new(page: @page) %> diff --git a/admin/app/components/solidus_admin/return_reasons/new/component.rb b/admin/app/components/solidus_admin/return_reasons/new/component.rb new file mode 100644 index 00000000000..7efe494f634 --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/new/component.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SolidusAdmin::ReturnReasons::New::Component < SolidusAdmin::BaseComponent + def initialize(page:, return_reason:) + @page = page + @return_reason = return_reason + end + + def form_id + dom_id(@return_reason, "#{stimulus_id}_new_return_reason_form") + end +end diff --git a/admin/app/components/solidus_admin/return_reasons/new/component.yml b/admin/app/components/solidus_admin/return_reasons/new/component.yml new file mode 100644 index 00000000000..bbed4e31820 --- /dev/null +++ b/admin/app/components/solidus_admin/return_reasons/new/component.yml @@ -0,0 +1,8 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + title: "New Return Reason" + cancel: "Cancel" + submit: "Add Return Reason" + hints: + active: "When checked, this return reason will be available for selection when returning orders." diff --git a/admin/app/controllers/solidus_admin/return_reasons_controller.rb b/admin/app/controllers/solidus_admin/return_reasons_controller.rb index 4887f236794..ef47465ecd3 100644 --- a/admin/app/controllers/solidus_admin/return_reasons_controller.rb +++ b/admin/app/controllers/solidus_admin/return_reasons_controller.rb @@ -4,19 +4,86 @@ module SolidusAdmin class ReturnReasonsController < SolidusAdmin::BaseController include SolidusAdmin::ControllerHelpers::Search - def index - return_reasons = apply_search_to( - Spree::ReturnReason.unscoped.order(id: :desc), - param: :q, - ) + before_action :find_return_reason, only: %i[edit update] - set_page_and_extract_portion_from(return_reasons) + def index + set_index_page respond_to do |format| format.html { render component('return_reasons/index').new(page: @page) } end end + def new + @return_reason = Spree::ReturnReason.new + + set_index_page + + respond_to do |format| + format.html { render component('return_reasons/new').new(page: @page, return_reason: @return_reason) } + end + end + + def create + @return_reason = Spree::ReturnReason.new(return_reason_params) + + if @return_reason.save + respond_to do |format| + flash[:notice] = t('.success') + + format.html do + redirect_to solidus_admin.return_reasons_path, status: :see_other + end + + format.turbo_stream do + render turbo_stream: '' + end + end + else + set_index_page + + respond_to do |format| + format.html do + page_component = component('return_reasons/new').new(page: @page, return_reason: @return_reason) + render page_component, status: :unprocessable_entity + end + end + end + end + + def edit + set_index_page + + respond_to do |format| + format.html { render component('return_reasons/edit').new(page: @page, return_reason: @return_reason) } + end + end + + def update + if @return_reason.update(return_reason_params) + respond_to do |format| + flash[:notice] = t('.success') + + format.html do + redirect_to solidus_admin.return_reasons_path, status: :see_other + end + + format.turbo_stream do + render turbo_stream: '' + end + end + else + set_index_page + + respond_to do |format| + format.html do + page_component = component('return_reasons/edit').new(page: @page, return_reason: @return_reason) + render page_component, status: :unprocessable_entity + end + end + end + end + def destroy @return_reason = Spree::ReturnReason.find_by!(id: params[:id]) @@ -28,8 +95,21 @@ def destroy private + def find_return_reason + @return_reason = Spree::ReturnReason.find(params[:id]) + end + + def set_index_page + return_reasons = apply_search_to( + Spree::ReturnReason.unscoped.order(id: :desc), + param: :q, + ) + + set_page_and_extract_portion_from(return_reasons) + end + def return_reason_params - params.require(:return_reason).permit(:return_reason_id, permitted_return_reason_attributes) + params.require(:return_reason).permit(:name, :active) end end end diff --git a/admin/config/locales/return_reasons.en.yml b/admin/config/locales/return_reasons.en.yml index 248dc9769d7..7987e6f786a 100644 --- a/admin/config/locales/return_reasons.en.yml +++ b/admin/config/locales/return_reasons.en.yml @@ -3,4 +3,8 @@ en: return_reasons: title: "Return Reasons" destroy: - success: "Return Reasons were successfully removed." + success: "Return reasons were successfully removed." + create: + success: "Return reason was successfully created." + update: + success: "Return reason was successfully updated." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index f82f324a81b..7ad5ef34c89 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -62,7 +62,7 @@ admin_resources :zones, only: [:index, :destroy] admin_resources :refund_reasons, except: [:show] admin_resources :reimbursement_types, only: [:index] - admin_resources :return_reasons, only: [:index, :destroy] + admin_resources :return_reasons, except: [:show] admin_resources :adjustment_reasons, except: [:show] admin_resources :store_credit_reasons, except: [:show] end diff --git a/admin/spec/features/return_reasons_spec.rb b/admin/spec/features/return_reasons_spec.rb index 3e929cd1dd4..5932101bf41 100644 --- a/admin/spec/features/return_reasons_spec.rb +++ b/admin/spec/features/return_reasons_spec.rb @@ -14,9 +14,82 @@ select_row("Default-return-reason") click_on "Delete" - expect(page).to have_content("Return Reasons were successfully removed.") + expect(page).to have_content("Return reasons were successfully removed.") expect(page).not_to have_content("Default-return-reason") expect(Spree::ReturnReason.count).to eq(0) expect(page).to be_axe_clean end + + context "when creating a new return reason" do + let(:query) { "?page=1&q%5Bname_cont%5D=new" } + + before do + visit "/admin/return_reasons#{query}" + click_on "Add new" + expect(page).to have_content("New Return Reason") + expect(page).to be_axe_clean + end + + it "opens a modal" do + expect(page).to have_selector("dialog") + within("dialog") { click_on "Cancel" } + expect(page).not_to have_selector("dialog") + expect(page.current_url).to include(query) + end + + context "with valid data" do + it "successfully creates a new return reason, keeping page and q params" do + fill_in "Name", with: "New Reason" + page.uncheck "return_reason[active]" + + click_on "Add Return Reason" + + expect(page).to have_content("Return reason was successfully created.") + expect(Spree::ReturnReason.find_by(name: "New Reason")).to be_present + expect(Spree::ReturnReason.find_by(name: "New Reason").active).to be_falsey + expect(page.current_url).to include(query) + end + end + + context "with invalid data" do + it "fails to create a new return reason, keeping page and q params" do + click_on "Add Return Reason" + + expect(page).to have_content("can't be blank") + expect(page.current_url).to include(query) + end + end + end + + context "when editing an existing return reason" do + let(:query) { "?page=1&q%5Bname_cont%5D=reason" } + + before do + Spree::ReturnReason.create(name: "Good Reason") + visit "/admin/return_reasons#{query}" + find_row("Good Reason").click + expect(page).to have_content("Edit Return Reason") + expect(page).to be_axe_clean + end + + it "opens a modal" do + expect(page).to have_selector("dialog") + within("dialog") { click_on "Cancel" } + expect(page).not_to have_selector("dialog") + expect(page.current_url).to include(query) + end + + it "successfully updates the existing return reason" do + fill_in "Name", with: "Better Reason" + page.uncheck "return_reason[active]" + + click_on "Update Return Reason" + expect(page).to have_content("Return reason was successfully updated.") + expect(page).to have_content("Better Reason") + expect(page).not_to have_content("Good Reason") + expect(Spree::ReturnReason.find_by(name: "Better Reason")).to be_present + expect(Spree::ReturnReason.find_by(name: "Better Reason").active).to be_falsey + expect(page.current_url).to include(query) + end + end end diff --git a/admin/spec/requests/solidus_admin/return_reasons_spec.rb b/admin/spec/requests/solidus_admin/return_reasons_spec.rb new file mode 100644 index 00000000000..179715484ba --- /dev/null +++ b/admin/spec/requests/solidus_admin/return_reasons_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "SolidusAdmin::ReturnReasonsController", type: :request do + let(:admin_user) { create(:admin_user) } + let(:return_reason) { create(:return_reason) } + + before do + allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) + end + + describe "GET /index" do + it "renders the index template with a 200 OK status" do + get solidus_admin.return_reasons_path + expect(response).to have_http_status(:ok) + end + end + + describe "GET /new" do + it "renders the new template with a 200 OK status" do + get solidus_admin.new_return_reason_path + expect(response).to have_http_status(:ok) + end + end + + describe "GET /edit" do + it "renders the edit template with a 200 OK status" do + get solidus_admin.edit_return_reason_path(return_reason) + expect(response).to have_http_status(:ok) + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:valid_attributes) { { name: "Updated Return Reason", active: false } } + + it "updates the return reason" do + patch solidus_admin.return_reason_path(return_reason), params: { return_reason: valid_attributes } + return_reason.reload + expect(return_reason.name).to eq("Updated Return Reason") + expect(return_reason.active).to be(false) + end + + it "redirects to the index page with a 303 See Other status" do + patch solidus_admin.return_reason_path(return_reason), params: { return_reason: valid_attributes } + expect(response).to redirect_to(solidus_admin.return_reasons_path) + expect(response).to have_http_status(:see_other) + end + + it "displays a success flash message" do + patch solidus_admin.return_reason_path(return_reason), params: { return_reason: valid_attributes } + follow_redirect! + expect(response.body).to include("Return reason was successfully updated.") + end + end + + context "with invalid parameters" do + let(:invalid_attributes) { { name: "", active: false } } + + it "does not update the return reason" do + original_name = return_reason.name + patch solidus_admin.return_reason_path(return_reason), params: { return_reason: invalid_attributes } + return_reason.reload + expect(return_reason.name).to eq(original_name) + end + + it "renders the edit template with unprocessable_entity status" do + patch solidus_admin.return_reason_path(return_reason), params: { return_reason: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "POST /create" do + context "with valid parameters" do + let(:valid_attributes) { { name: "Damaged item", active: true } } + + it "creates a new ReturnReason" do + expect { + post solidus_admin.return_reasons_path, params: { return_reason: valid_attributes } + }.to change(Spree::ReturnReason, :count).by(1) + end + + it "redirects to the index page with a 303 See Other status" do + post solidus_admin.return_reasons_path, params: { return_reason: valid_attributes } + expect(response).to redirect_to(solidus_admin.return_reasons_path) + expect(response).to have_http_status(:see_other) + end + + it "displays a success flash message" do + post solidus_admin.return_reasons_path, params: { return_reason: valid_attributes } + follow_redirect! + expect(response.body).to include("Return reason was successfully created.") + end + end + + context "with invalid parameters" do + let(:invalid_attributes) { { name: "" } } + + it "does not create a new ReturnReason" do + expect { + post solidus_admin.return_reasons_path, params: { return_reason: invalid_attributes } + }.not_to change(Spree::ReturnReason, :count) + end + + it "renders the new template with unprocessable_entity status" do + post solidus_admin.return_reasons_path, params: { return_reason: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "DELETE /destroy" do + let!(:return_reason_to_delete) { create(:return_reason) } + + it "deletes the return reason and redirects to the index page with a 303 See Other status" do + expect { + delete solidus_admin.return_reason_path(return_reason_to_delete) + }.to change(Spree::ReturnReason, :count).by(-1) + + expect(response).to redirect_to(solidus_admin.return_reasons_path) + expect(response).to have_http_status(:see_other) + end + + it "displays a success flash message after deletion" do + delete solidus_admin.return_reason_path(return_reason_to_delete) + follow_redirect! + expect(response.body).to include("Return reasons were successfully removed.") + end + end +end