From eb6f7342be31317fcf7221f3a06c5014768f679d Mon Sep 17 00:00:00 2001 From: Madeline Collier Date: Wed, 14 Aug 2024 19:38:31 +0200 Subject: [PATCH 1/2] Add nesting for "Users and Roles" admin component Now instead of a top level Users component, the main landing page is "Users and Roles" (with the users page being the pre-selected tab, so the only visual change is a new page header and a new tab component to swap between "Users" and "Roles". --- .../solidus_admin/users/index/component.rb | 2 +- .../users_and_roles/component.rb | 19 +++++++++++++++++++ .../users_and_roles/component.yml | 2 ++ admin/spec/features/users_spec.rb | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 admin/app/components/solidus_admin/users_and_roles/component.rb create mode 100644 admin/app/components/solidus_admin/users_and_roles/component.yml diff --git a/admin/app/components/solidus_admin/users/index/component.rb b/admin/app/components/solidus_admin/users/index/component.rb index 5b555b0912e..46b20878508 100644 --- a/admin/app/components/solidus_admin/users/index/component.rb +++ b/admin/app/components/solidus_admin/users/index/component.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SolidusAdmin::Users::Index::Component < SolidusAdmin::UI::Pages::Index::Component +class SolidusAdmin::Users::Index::Component < SolidusAdmin::UsersAndRoles::Component def model_class Spree.user_class end diff --git a/admin/app/components/solidus_admin/users_and_roles/component.rb b/admin/app/components/solidus_admin/users_and_roles/component.rb new file mode 100644 index 00000000000..dbcf6bd7882 --- /dev/null +++ b/admin/app/components/solidus_admin/users_and_roles/component.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class SolidusAdmin::UsersAndRoles::Component < SolidusAdmin::UI::Pages::Index::Component + def title + page_header_title safe_join([ + tag.div(t(".title")), + ]) + end + + def tabs + [ + { + text: Spree.user_class.model_name.human(count: 2), + href: solidus_admin.users_path, + current: model_class == Spree.user_class, + }, + ] + end +end diff --git a/admin/app/components/solidus_admin/users_and_roles/component.yml b/admin/app/components/solidus_admin/users_and_roles/component.yml new file mode 100644 index 00000000000..5fa0dabe81b --- /dev/null +++ b/admin/app/components/solidus_admin/users_and_roles/component.yml @@ -0,0 +1,2 @@ +en: + title: "Users and Roles" diff --git a/admin/spec/features/users_spec.rb b/admin/spec/features/users_spec.rb index ceef0b54d75..2149c1a20b7 100644 --- a/admin/spec/features/users_spec.rb +++ b/admin/spec/features/users_spec.rb @@ -11,6 +11,7 @@ create(:user, :with_orders, email: "customer-with-order@example.com") visit "/admin/users" + expect(page).to have_content("Users and Roles") expect(page).to have_content("customer@example.com") expect(page).not_to have_content("admin-2@example.com") click_on "Admins" From 1fbe6d553d825c9ed571729c9991773559b4e680 Mon Sep 17 00:00:00 2001 From: Madeline Collier Date: Wed, 14 Aug 2024 19:40:58 +0200 Subject: [PATCH 2/2] Add Role component to the new admin This includes the index, the new/create logic, and the singular or bulk deletion logic as well as the associated components and specs. --- .../solidus_admin/roles/index/component.rb | 65 ++++++++++++++ .../solidus_admin/roles/index/component.yml | 6 ++ .../roles/new/component.html.erb | 17 ++++ .../solidus_admin/roles/new/component.rb | 12 +++ .../solidus_admin/roles/new/component.yml | 6 ++ .../users_and_roles/component.rb | 5 ++ .../solidus_admin/roles_controller.rb | 79 +++++++++++++++++ admin/config/locales/roles.en.yml | 8 ++ admin/config/routes.rb | 1 + admin/spec/features/roles_spec.rb | 77 +++++++++++++++++ .../spec/requests/solidus_admin/roles_spec.rb | 85 +++++++++++++++++++ 11 files changed, 361 insertions(+) create mode 100644 admin/app/components/solidus_admin/roles/index/component.rb create mode 100644 admin/app/components/solidus_admin/roles/index/component.yml create mode 100644 admin/app/components/solidus_admin/roles/new/component.html.erb create mode 100644 admin/app/components/solidus_admin/roles/new/component.rb create mode 100644 admin/app/components/solidus_admin/roles/new/component.yml create mode 100644 admin/app/controllers/solidus_admin/roles_controller.rb create mode 100644 admin/config/locales/roles.en.yml create mode 100644 admin/spec/features/roles_spec.rb create mode 100644 admin/spec/requests/solidus_admin/roles_spec.rb diff --git a/admin/app/components/solidus_admin/roles/index/component.rb b/admin/app/components/solidus_admin/roles/index/component.rb new file mode 100644 index 00000000000..6d2063e0fc0 --- /dev/null +++ b/admin/app/components/solidus_admin/roles/index/component.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class SolidusAdmin::Roles::Index::Component < SolidusAdmin::UsersAndRoles::Component + def model_class + Spree::Role + end + + def search_key + :name_cont + end + + def search_url + solidus_admin.roles_path + end + + def row_url(role) + solidus_admin.roles_path(role) + end + + def page_actions + render component("ui/button").new( + tag: :a, + text: t('.add'), + href: solidus_admin.new_role_path, data: { turbo_frame: :new_role_modal }, + icon: "add-line", + ) + end + + def turbo_frames + %w[ + new_role_modal + ] + end + + def batch_actions + [ + { + label: t('.batch_actions.delete'), + action: solidus_admin.roles_path, + method: :delete, + icon: 'delete-bin-7-line', + }, + ] + end + + def scopes + [ + { name: :all, label: t('.scopes.all'), default: true }, + { name: :admin, label: t('.scopes.admin') }, + ] + end + + def filters + [] + end + + def columns + [ + { + header: :role, + data: :name, + } + ] + end +end diff --git a/admin/app/components/solidus_admin/roles/index/component.yml b/admin/app/components/solidus_admin/roles/index/component.yml new file mode 100644 index 00000000000..3934c0825e9 --- /dev/null +++ b/admin/app/components/solidus_admin/roles/index/component.yml @@ -0,0 +1,6 @@ +en: + batch_actions: + delete: 'Delete' + scopes: + admin: Admin + all: All diff --git a/admin/app/components/solidus_admin/roles/new/component.html.erb b/admin/app/components/solidus_admin/roles/new/component.html.erb new file mode 100644 index 00000000000..dcd5d78d38c --- /dev/null +++ b/admin/app/components/solidus_admin/roles/new/component.html.erb @@ -0,0 +1,17 @@ +<%= turbo_frame_tag :new_role_modal do %> + <%= render component("ui/modal").new(title: t(".title")) do |modal| %> + <%= form_for @role, url: solidus_admin.roles_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("roles/index").new(page: @page) %> diff --git a/admin/app/components/solidus_admin/roles/new/component.rb b/admin/app/components/solidus_admin/roles/new/component.rb new file mode 100644 index 00000000000..8c849014bf4 --- /dev/null +++ b/admin/app/components/solidus_admin/roles/new/component.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SolidusAdmin::Roles::New::Component < SolidusAdmin::BaseComponent + def initialize(page:, role:) + @page = page + @role = role + end + + def form_id + dom_id(@role, "#{stimulus_id}_new_role_form") + end +end diff --git a/admin/app/components/solidus_admin/roles/new/component.yml b/admin/app/components/solidus_admin/roles/new/component.yml new file mode 100644 index 00000000000..6c318631b74 --- /dev/null +++ b/admin/app/components/solidus_admin/roles/new/component.yml @@ -0,0 +1,6 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + title: "New Role" + cancel: "Cancel" + submit: "Add Role" diff --git a/admin/app/components/solidus_admin/users_and_roles/component.rb b/admin/app/components/solidus_admin/users_and_roles/component.rb index dbcf6bd7882..450aa352d0c 100644 --- a/admin/app/components/solidus_admin/users_and_roles/component.rb +++ b/admin/app/components/solidus_admin/users_and_roles/component.rb @@ -14,6 +14,11 @@ def tabs href: solidus_admin.users_path, current: model_class == Spree.user_class, }, + { + text: Spree::Role.model_name.human(count: 2), + href: solidus_admin.roles_path, + current: model_class == Spree::Role, + }, ] end end diff --git a/admin/app/controllers/solidus_admin/roles_controller.rb b/admin/app/controllers/solidus_admin/roles_controller.rb new file mode 100644 index 00000000000..f01cf30cb27 --- /dev/null +++ b/admin/app/controllers/solidus_admin/roles_controller.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module SolidusAdmin + class RolesController < SolidusAdmin::BaseController + include SolidusAdmin::ControllerHelpers::Search + + search_scope(:all) + search_scope(:admin) { _1.where(name: "admin") } + + def index + set_index_page + + respond_to do |format| + format.html { render component('roles/index').new(page: @page) } + end + end + + def new + @role = Spree::Role.new + + set_index_page + + respond_to do |format| + format.html { render component('roles/new').new(page: @page, role: @role) } + end + end + + def create + @role = Spree::Role.new(role_params) + + if @role.save + respond_to do |format| + flash[:notice] = t('.success') + + format.html do + redirect_to solidus_admin.roles_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('roles/new').new(page: @page, role: @role) + render page_component, status: :unprocessable_entity + end + end + end + end + + def destroy + @roles = Spree::Role.where(id: params[:id]) + + Spree::Role.transaction { @roles.destroy_all } + + flash[:notice] = t('.success') + redirect_back_or_to solidus_admin.roles_path, status: :see_other + end + + private + + def set_index_page + roles = apply_search_to( + Spree::Role.unscoped.order(id: :desc), + param: :q, + ) + + set_page_and_extract_portion_from(roles) + end + + def role_params + params.require(:role).permit(:role_id, :name, :description, :type) + end + end +end diff --git a/admin/config/locales/roles.en.yml b/admin/config/locales/roles.en.yml new file mode 100644 index 00000000000..3017e99ac94 --- /dev/null +++ b/admin/config/locales/roles.en.yml @@ -0,0 +1,8 @@ +en: + solidus_admin: + roles: + title: "Roles" + destroy: + success: "Roles were successfully removed." + create: + success: "Role was successfully created." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 7ad5ef34c89..e2c6e716a05 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -63,6 +63,7 @@ admin_resources :refund_reasons, except: [:show] admin_resources :reimbursement_types, only: [:index] admin_resources :return_reasons, except: [:show] + admin_resources :roles, only: [:index, :new, :create, :destroy] admin_resources :adjustment_reasons, except: [:show] admin_resources :store_credit_reasons, except: [:show] end diff --git a/admin/spec/features/roles_spec.rb b/admin/spec/features/roles_spec.rb new file mode 100644 index 00000000000..a05e4f88c47 --- /dev/null +++ b/admin/spec/features/roles_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe "Roles", :js, type: :feature do + before { sign_in create(:admin_user, email: 'admin@example.com') } + + it "lists roles and allows deleting them" do + create(:role, name: "Customer Role" ) + Spree::Role.find_or_create_by(name: 'admin') + + visit "/admin/roles" + expect(page).to have_content("Users and Roles") + expect(page).to have_content("Customer Role") + expect(page).to have_content("admin") + click_on "Admin" + expect(page).to have_content("admin") + expect(page).not_to have_content("Customer Role") + click_on "All" + expect(page).to have_content("Customer Role") + expect(page).to have_content("admin") + + expect(page).to be_axe_clean + + select_row("Customer Role") + click_on "Delete" + expect(page).to have_content("Roles were successfully removed.") + expect(page).not_to have_content("Customer Role") + expect(Spree::Role.count).to eq(1) + end + + context "when creating a role" do + let(:query) { "?page=1&q%5Bname_cont%5D=new" } + + before do + visit "/admin/roles#{query}" + click_on "Add new" + expect(page).to have_content("New Role") + 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 role, keeping page and q params" do + fill_in "Name", with: "Purchaser" + + click_on "Add Role" + + expect(page).to have_content("Role was successfully created.") + expect(Spree::Role.find_by(name: "Purchaser")).to be_present + expect(page.current_url).to include(query) + end + end + + context "with invalid data" do + # @note: The only validation that Roles currently have is that names must + # be unique (but they can still be blank). + before do + create(:role, name: "Customer Role" ) + end + + it "fails to create a new role, keeping page and q params" do + fill_in "Name", with: "Customer Role" + click_on "Add Role" + + expect(page).to have_content("has already been taken") + expect(page.current_url).to include(query) + end + end + end +end diff --git a/admin/spec/requests/solidus_admin/roles_spec.rb b/admin/spec/requests/solidus_admin/roles_spec.rb new file mode 100644 index 00000000000..fea41daabf6 --- /dev/null +++ b/admin/spec/requests/solidus_admin/roles_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "SolidusAdmin::RolesController", type: :request do + let(:admin_user) { create(:admin_user) } + let(:role) { create(:role) } + + before do + allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) + Spree::Role.find_or_create_by(name: 'admin') + end + + describe "GET /index" do + it "renders the index template with a 200 OK status" do + get solidus_admin.roles_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_role_path + expect(response).to have_http_status(:ok) + end + end + + describe "POST /create" do + context "with valid parameters" do + let(:valid_attributes) { { name: "Customer" } } + + it "creates a new Role" do + expect { + post solidus_admin.roles_path, params: { role: valid_attributes } + }.to change(Spree::Role, :count).by(1) + end + + it "redirects to the index page with a 303 See Other status" do + post solidus_admin.roles_path, params: { role: valid_attributes } + expect(response).to redirect_to(solidus_admin.roles_path) + expect(response).to have_http_status(:see_other) + end + + it "displays a success flash message" do + post solidus_admin.roles_path, params: { role: valid_attributes } + follow_redirect! + expect(response.body).to include("Role was successfully created.") + end + end + + context "with invalid parameters" do + let(:invalid_attributes) { { name: "admin" } } + + it "does not create a new Role" do + expect { + post solidus_admin.roles_path, params: { role: invalid_attributes } + }.not_to change(Spree::Role, :count) + end + + it "renders the new template with unprocessable_entity status" do + post solidus_admin.roles_path, params: { role: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "DELETE /destroy" do + let!(:role_to_delete) { create(:role) } + + it "deletes the role and redirects to the index page with a 303 See Other status" do + expect { + delete solidus_admin.role_path(role_to_delete) + }.to change(Spree::Role, :count).by(-1) + + expect(response).to redirect_to(solidus_admin.roles_path) + expect(response).to have_http_status(:see_other) + end + + it "displays a success flash message after deletion" do + delete solidus_admin.role_path(role_to_delete) + follow_redirect! + expect(response.body).to include("Roles were successfully removed.") + end + end +end