diff --git a/app/helpers/spree/admin/navigation_helper.rb b/app/helpers/spree/admin/navigation_helper.rb index 7da7eba5a9..0060742ff6 100644 --- a/app/helpers/spree/admin/navigation_helper.rb +++ b/app/helpers/spree/admin/navigation_helper.rb @@ -308,6 +308,18 @@ def page_header_back_button(url) def main_menu Rails.application.config.spree_backend.main_menu end + + def order_tabs + Rails.application.config.spree_backend.tabs[:order] + end + + def user_tabs + Rails.application.config.spree_backend.tabs[:user] + end + + def product_tabs + Rails.application.config.spree_backend.tabs[:product] + end end end end diff --git a/app/models/spree/admin/item_manager.rb b/app/models/spree/admin/item_manager.rb new file mode 100644 index 0000000000..49d7c3f147 --- /dev/null +++ b/app/models/spree/admin/item_manager.rb @@ -0,0 +1,50 @@ +module Spree + module Admin + module ItemManager + def add(item) + raise KeyError, "Item with key #{item.key} already exists" if index_for_key(item.key) + + @items << item + end + + def child_with_key?(key) + index_for_key(key).present? + end + + def remove(item_key) + item_index = index_for_key!(item_key) + + @items.delete_at(item_index) + end + + def item_for_key(key) + @items.find { |e| e.key == key } + end + + def insert_before(item_key, item_to_add) + item_index = index_for_key!(item_key) + + @items.insert(item_index, item_to_add) + end + + def insert_after(item_key, item_to_add) + item_index = index_for_key!(item_key) + + @items.insert(item_index + 1, item_to_add) + end + + private + + def index_for_key(key) + @items.index { |e| e.key == key } + end + + def index_for_key!(key) + item_index = index_for_key(key) + raise KeyError, "Item not found for key #{key}" unless item_index + + item_index + end + end + end +end diff --git a/app/models/spree/admin/main_menu/availability_builder_methods.rb b/app/models/spree/admin/main_menu/availability_builder_methods.rb deleted file mode 100644 index 69a9bd2af7..0000000000 --- a/app/models/spree/admin/main_menu/availability_builder_methods.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Spree - module Admin - module MainMenu - module AvailabilityBuilderMethods - def with_availability_check(availability_check) - @availability_check = availability_check - self - end - - def with_manage_ability_check(*any_of_resources) - @availability_check = ->(ability, _store) { any_of_resources.any? { |r| ability.can?(:manage, r) } } - self - end - - def with_admin_ability_check(*any_of_resources) - @availability_check = ->(ability, _store) { any_of_resources.any? { |r| ability.can?(:admin, r) } } - self - end - end - end - end -end diff --git a/app/models/spree/admin/main_menu/item_builder.rb b/app/models/spree/admin/main_menu/item_builder.rb index 46aeec9c07..d3ed00c0ba 100644 --- a/app/models/spree/admin/main_menu/item_builder.rb +++ b/app/models/spree/admin/main_menu/item_builder.rb @@ -2,7 +2,7 @@ module Spree module Admin module MainMenu class ItemBuilder - include AvailabilityBuilderMethods + include ::Spree::Admin::PermissionChecks def initialize(key, url) @key = key diff --git a/app/models/spree/admin/main_menu/root.rb b/app/models/spree/admin/main_menu/root.rb index 86af3528e1..188521a511 100644 --- a/app/models/spree/admin/main_menu/root.rb +++ b/app/models/spree/admin/main_menu/root.rb @@ -9,7 +9,7 @@ def initialize end def add_to_section(section_key, item) - @items.find { |e| e.key == section_key }.add_item(item) + @items.find { |e| e.key == section_key }.add(item) end end end diff --git a/app/models/spree/admin/main_menu/section.rb b/app/models/spree/admin/main_menu/section.rb index 30b812d833..7146556fdf 100644 --- a/app/models/spree/admin/main_menu/section.rb +++ b/app/models/spree/admin/main_menu/section.rb @@ -2,6 +2,8 @@ module Spree module Admin module MainMenu class Section + include ::Spree::Admin::ItemManager + attr_reader :key, :label_translation_key, :icon_key, :items def initialize(key, label_translation_key, icon_key, availability_check, items) @@ -12,38 +14,6 @@ def initialize(key, label_translation_key, icon_key, availability_check, items) @items = items end - def add(item) - raise KeyError, "Item with key #{key} already exists" if index_for_key(item.key) - - @items << item - end - - def child_with_key?(key) - index_for_key(key).present? - end - - def remove(item_key) - item_index = index_for_key!(item_key) - - @items.delete_at(item_index) - end - - def item_for_key(key) - @items.find { |e| e.key == key } - end - - def insert_before(item_key, item_to_add) - item_index = index_for_key!(item_key) - - @items.insert(item_index, item_to_add) - end - - def insert_after(item_key, item_to_add) - item_index = index_for_key!(item_key) - - @items.insert(item_index + 1, item_to_add) - end - def available?(current_ability, current_store) return true unless @availability_check.present? @@ -53,19 +23,6 @@ def available?(current_ability, current_store) def children? @items.any? end - - private - - def index_for_key(key) - @items.index { |e| e.key == key } - end - - def index_for_key!(key) - item_index = index_for_key(key) - raise KeyError, "Item not found for key #{key}" unless item_index - - item_index - end end end end diff --git a/app/models/spree/admin/main_menu/section_builder.rb b/app/models/spree/admin/main_menu/section_builder.rb index e147a5a544..d3864a2eb7 100644 --- a/app/models/spree/admin/main_menu/section_builder.rb +++ b/app/models/spree/admin/main_menu/section_builder.rb @@ -2,7 +2,7 @@ module Spree module Admin module MainMenu class SectionBuilder - include AvailabilityBuilderMethods + include ::Spree::Admin::PermissionChecks def initialize(key, icon_key) @key = key diff --git a/app/models/spree/admin/permission_checks.rb b/app/models/spree/admin/permission_checks.rb new file mode 100644 index 0000000000..475e27096f --- /dev/null +++ b/app/models/spree/admin/permission_checks.rb @@ -0,0 +1,30 @@ +module Spree + module Admin + module PermissionChecks + def with_availability_check(availability_check) + @availability_check = availability_check + self + end + + def with_manage_ability_check(*classes) + @availability_check = ->(ability, _current) { classes.any? { |c| ability.can?(:manage, c) } } + self + end + + def with_admin_ability_check(*classes) + @availability_check = ->(ability, _current) { classes.any? { |c| ability.can?(:admin, c) } } + self + end + + def with_index_ability_check(*classes) + @availability_check = ->(ability, _current) { classes.any? { |c| ability.can?(:index, c) } } + self + end + + def with_update_ability_check + @availability_check = ->(ability, resource) { ability.can?(:update, resource) } + self + end + end + end +end diff --git a/app/models/spree/admin/tabs/conditional_checker.rb b/app/models/spree/admin/tabs/conditional_checker.rb new file mode 100644 index 0000000000..e17b65d362 --- /dev/null +++ b/app/models/spree/admin/tabs/conditional_checker.rb @@ -0,0 +1,17 @@ +module Spree + module Admin + module Tabs + module ConditionalChecker + def with_active_check + @active_check = ->(current_tab, text) { current_tab == text } + self + end + + def with_completed_check + @completed_check = ->(resource) { resource.completed? } + self + end + end + end + end +end diff --git a/app/models/spree/admin/tabs/data_hook.rb b/app/models/spree/admin/tabs/data_hook.rb new file mode 100644 index 0000000000..16bcd67c83 --- /dev/null +++ b/app/models/spree/admin/tabs/data_hook.rb @@ -0,0 +1,12 @@ +module Spree + module Admin + module Tabs + module DataHook + def with_data_hook(data_hook) + @data_hook = data_hook + self + end + end + end + end +end diff --git a/app/models/spree/admin/tabs/order_default_tabs_builder.rb b/app/models/spree/admin/tabs/order_default_tabs_builder.rb new file mode 100644 index 0000000000..35ba049275 --- /dev/null +++ b/app/models/spree/admin/tabs/order_default_tabs_builder.rb @@ -0,0 +1,218 @@ +module Spree + module Admin + module Tabs + # rubocop:disable Metrics/ClassLength + class OrderDefaultTabsBuilder + include Spree::Core::Engine.routes.url_helpers + + def build + root = Root.new + add_cart_tab(root) + add_channel_tab(root) + add_customer_tab(root) + add_shipments_tab(root) + add_adjustments_tab(root) + add_payments_tab(root) + add_return_authorizations_tab(root) + add_customer_returns_tab(root) + add_state_changes_tab(root) + root + end + + private + + def add_cart_tab(root) + tab = + TabBuilder.new(cart_tab_config). + with_active_check. + with_availability_check( + # An abstract module should not be aware of resource's internal structure. + # If these checks are elaborate, it's better to have this complexity declared explicitly here. + lambda do |ability, resource| + ability.can?(:update, resource) && (resource.shipments.empty? || resource.shipments.shipped.empty?) + end + ). + with_data_hook('admin_order_tabs_cart_details'). + build + + root.add(tab) + end + + def cart_tab_config + { + icon_name: 'cart-check.svg', + key: :cart, + url: ->(resource) { cart_admin_order_path(resource) }, + partial_name: :cart + } + end + + def add_channel_tab(root) + tab = + TabBuilder.new(channel_tab_config). + with_active_check. + with_update_ability_check. + with_data_hook('admin_order_tabs_channel_details'). + build + + root.add(tab) + end + + def channel_tab_config + { + icon_name: 'funnel.svg', + key: :channel, + url: ->(resource) { channel_admin_order_path(resource) }, + partial_name: :channel + } + end + + def add_customer_tab(root) + tab = + TabBuilder.new(customer_tab_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:update, resource) && resource.checkout_steps.include?('address') + end + ). + with_data_hook('admin_order_tabs_customer_details'). + build + + root.add(tab) + end + + def customer_tab_config + { + icon_name: 'person-lines-fill.svg', + key: :customer, + url: ->(resource) { admin_order_customer_path(resource) }, + partial_name: :customer_details + } + end + + def add_shipments_tab(root) + tab = + TabBuilder.new(shipments_tab_config). + with_active_check. + with_update_ability_check. + with_data_hook('admin_order_tabs_shipment_details'). + build + + root.add(tab) + end + + def shipments_tab_config + { + icon_name: 'truck.svg', + key: :shipments, + url: ->(resource) { edit_admin_order_path(resource) }, + partial_name: :shipments + } + end + + def add_adjustments_tab(root) + tab = + TabBuilder.new(adjustments_tab_config). + with_active_check. + with_index_ability_check(::Spree::Adjustment). + with_data_hook('admin_order_tabs_adjustments'). + build + + root.add(tab) + end + + def adjustments_tab_config + { + icon_name: 'adjust.svg', + key: :adjustments, + url: ->(resource) { admin_order_adjustments_path(resource) }, + partial_name: :adjustments + } + end + + def add_payments_tab(root) + tab = + TabBuilder.new(payments_tab_config). + with_active_check. + with_index_ability_check(::Spree::Payment). + with_data_hook('admin_order_tabs_payments'). + build + + root.add(tab) + end + + def payments_tab_config + { + icon_name: 'credit-card.svg', + key: :payments, + url: ->(resource) { admin_order_payments_path(resource) }, + partial_name: :payments + } + end + + def add_return_authorizations_tab(root) + tab = + TabBuilder.new(return_authorizations_tab_config). + with_active_check. + with_completed_check. + with_index_ability_check(::Spree::ReturnAuthorization). + with_data_hook('admin_order_tabs_return_authorizations'). + build + + root.add(tab) + end + + def return_authorizations_tab_config + { + icon_name: 'enter.svg', + key: :return_authorizations, + url: ->(resource) { admin_order_return_authorizations_path(resource) }, + partial_name: :return_authorizations + } + end + + def add_customer_returns_tab(root) + tab = + TabBuilder.new(customer_returns_tab_config). + with_active_check. + with_completed_check. + with_index_ability_check(::Spree::CustomerReturn). + build + + root.add(tab) + end + + def customer_returns_tab_config + { + icon_name: 'returns.svg', + key: :customer_returns, + url: ->(resource) { admin_order_customer_returns_path(resource) }, + partial_name: :customer_returns + } + end + + def add_state_changes_tab(root) + tab = + TabBuilder.new(state_changes_tab_config). + with_active_check. + with_update_ability_check. + with_data_hook('admin_order_tabs_state_changes'). + build + + root.add(tab) + end + + def state_changes_tab_config + { + icon_name: 'calendar3.svg', + key: :state_changes, + url: ->(resource) { admin_order_state_changes_path(resource) }, + partial_name: :state_changes + } + end + end + # rubocop:enable Metrics/ClassLength + end + end +end diff --git a/app/models/spree/admin/tabs/product_default_tabs_builder.rb b/app/models/spree/admin/tabs/product_default_tabs_builder.rb new file mode 100644 index 0000000000..e6c2380874 --- /dev/null +++ b/app/models/spree/admin/tabs/product_default_tabs_builder.rb @@ -0,0 +1,206 @@ +module Spree + module Admin + module Tabs + # rubocop:disable Metrics/ClassLength + class ProductDefaultTabsBuilder + include Spree::Core::Engine.routes.url_helpers + + def build + root = Root.new + add_details_tab(root) + add_images_tab(root) + add_variants_tab(root) + add_properties_tab(root) + add_stock_tab(root) + add_prices_tab(root) + add_digitals_tab(root) + add_translations_tab(root) + root + end + + private + + def add_details_tab(root) + tab = + TabBuilder.new(details_config). + with_active_check. + with_admin_ability_check(::Spree::Product). + build + + root.add(tab) + end + + def details_config + { + icon_name: 'edit.svg', + key: :details, + url: ->(resource) { edit_admin_product_path(resource) }, + partial_name: :details + } + end + + def add_images_tab(root) + tab = + TabBuilder.new(images_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::Image) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def images_config + { + icon_name: 'images.svg', + key: :images, + url: ->(resource) { admin_product_images_path(resource) }, + partial_name: :images + } + end + + def add_variants_tab(root) + tab = + TabBuilder.new(variants_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::Variant) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def variants_config + { + icon_name: 'adjust.svg', + key: :variants, + url: ->(resource) { admin_product_variants_path(resource) }, + partial_name: :variants + } + end + + def add_properties_tab(root) + tab = + TabBuilder.new(properties_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::ProductProperty) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def properties_config + { + icon_name: 'list.svg', + key: :properties, + url: ->(resource) { admin_product_product_properties_path(resource) }, + partial_name: :properties + } + end + + def add_stock_tab(root) + tab = + TabBuilder.new(stock_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::StockItem) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def stock_config + { + icon_name: 'box-seam.svg', + key: :stock, + url: ->(resource) { stock_admin_product_path(resource) }, + partial_name: :stock + } + end + + def add_prices_tab(root) + tab = + TabBuilder.new(prices_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::Price) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def prices_config + { + icon_name: 'currency-exchange.svg', + key: :prices, + url: ->(resource) { admin_product_prices_path(resource) }, + partial_name: :prices + } + end + + def add_digitals_tab(root) + tab = + TabBuilder.new(digitals_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::Digital) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def digitals_config + { + icon_name: 'download.svg', + key: 'admin.digitals.digital_assets', + url: ->(resource) { admin_product_digitals_path(resource) }, + partial_name: :digitals + } + end + + def add_translations_tab(root) + tab = + TabBuilder.new(translations_config). + with_active_check. + with_availability_check( + lambda do |ability, resource| + ability.can?(:admin, ::Spree::Product) && !resource.deleted? + end + ). + build + + root.add(tab) + end + + def translations_config + { + icon_name: 'translate.svg', + key: :translations, + url: ->(resource) { translations_admin_product_path(resource) }, + partial_name: :translations + } + end + end + # rubocop:enable Metrics/ClassLength + end + end +end diff --git a/app/models/spree/admin/tabs/root.rb b/app/models/spree/admin/tabs/root.rb new file mode 100644 index 0000000000..969ca69887 --- /dev/null +++ b/app/models/spree/admin/tabs/root.rb @@ -0,0 +1,15 @@ +module Spree + module Admin + module Tabs + class Root + include ::Spree::Admin::ItemManager + + attr_reader :items + + def initialize + @items = [] + end + end + end + end +end diff --git a/app/models/spree/admin/tabs/tab.rb b/app/models/spree/admin/tabs/tab.rb new file mode 100644 index 0000000000..b44ee61c31 --- /dev/null +++ b/app/models/spree/admin/tabs/tab.rb @@ -0,0 +1,48 @@ +module Spree + module Admin + module Tabs + class Tab + attr_reader :icon_name, :key, :classes, :text, :data_hook + + def initialize(config) + @icon_name = config[:icon_name] + @key = config[:key] + @url = config[:url] + @partial_name = config[:partial_name] + @availability_check = config[:availability_check] + @active_check = config[:active_check] + @completed_check = config[:completed_check] + @text = config[:text] + @data_hook = config[:data_hook] + @classes = css_classes + end + + def available?(current_ability, resource) + return true unless @availability_check.present? + + @availability_check.call(current_ability, resource) + end + + def url(resource = nil) + @url.is_a?(Proc) ? @url.call(resource) : @url + end + + def active?(current_tab) + @active_check.call(current_tab, @partial_name) + end + + def complete?(resource) + return true unless @completed_check.present? + + @completed_check.call(resource) + end + + private + + def css_classes + 'nav-link' + end + end + end + end +end diff --git a/app/models/spree/admin/tabs/tab_builder.rb b/app/models/spree/admin/tabs/tab_builder.rb new file mode 100644 index 0000000000..86d8c99b19 --- /dev/null +++ b/app/models/spree/admin/tabs/tab_builder.rb @@ -0,0 +1,49 @@ +module Spree + module Admin + module Tabs + class TabBuilder + include ConditionalChecker + include ::Spree::Admin::PermissionChecks + include DataHook + + def initialize(config) + @icon_name = config[:icon_name] + @key = config[:key] + @url = config[:url] + @partial_name = config[:partial_name] + @availability_check = nil + @active_check = nil + @completed_check = nil + end + + def build + Tab.new(build_config) + end + + private + + def build_config + { + icon_name: @icon_name, + key: @key, + url: @url, + partial_name: @partial_name, + availability_check: @availability_check, + active_check: @active_check, + completed_check: @completed_check, + text: text, + data_hook: data_hook + } + end + + def text + ::Spree.t(@key) + end + + def data_hook + @data_hook.presence + end + end + end + end +end diff --git a/app/models/spree/admin/tabs/user_default_tabs_builder.rb b/app/models/spree/admin/tabs/user_default_tabs_builder.rb new file mode 100644 index 0000000000..ea7f0faefe --- /dev/null +++ b/app/models/spree/admin/tabs/user_default_tabs_builder.rb @@ -0,0 +1,111 @@ +module Spree + module Admin + module Tabs + class UserDefaultTabsBuilder + include Spree::Core::Engine.routes.url_helpers + + def build + root = Root.new + add_account_tab(root) + add_addresses_tab(root) + add_orders_tab(root) + add_items_tab(root) + add_store_credits_tab(root) + root + end + + private + + def add_account_tab(root) + tab = + TabBuilder.new(account_config). + with_active_check. + build + + root.add(tab) + end + + def account_config + { + icon_name: 'person.svg', + key: 'admin.user.account', + url: ->(resource) { edit_admin_user_path(resource) }, + partial_name: :account + } + end + + def add_addresses_tab(root) + tab = + TabBuilder.new(addresses_config). + with_active_check. + build + + root.add(tab) + end + + def addresses_config + { + icon_name: 'pin-map.svg', + key: 'admin.user.addresses', + url: ->(resource) { addresses_admin_user_path(resource) }, + partial_name: :address + } + end + + def add_orders_tab(root) + tab = + TabBuilder.new(orders_config). + with_active_check. + build + + root.add(tab) + end + + def orders_config + { + icon_name: 'inbox.svg', + key: 'admin.user.orders', + url: ->(resource) { orders_admin_user_path(resource) }, + partial_name: :orders + } + end + + def add_items_tab(root) + tab = + TabBuilder.new(items_config). + with_active_check. + build + + root.add(tab) + end + + def items_config + { + icon_name: 'tag.svg', + key: 'admin.user.items', + url: ->(resource) { items_admin_user_path(resource) }, + partial_name: :items + } + end + + def add_store_credits_tab(root) + tab = + TabBuilder.new(store_credits_config). + with_active_check. + build + + root.add(tab) + end + + def store_credits_config + { + icon_name: 'gift.svg', + key: 'admin.user.store_credits', + url: ->(resource) { admin_user_store_credits_path(resource) }, + partial_name: :store_credits + } + end + end + end + end +end diff --git a/app/views/spree/admin/shared/_order_tabs.html.erb b/app/views/spree/admin/shared/_order_tabs.html.erb index 91d0fa80ea..36871b7678 100644 --- a/app/views/spree/admin/shared/_order_tabs.html.erb +++ b/app/views/spree/admin/shared/_order_tabs.html.erb @@ -4,88 +4,15 @@ <% end %> <% content_for :page_tabs do %> - <% if ((can? :update, @order) && (@order.shipments.size.zero? || @order.shipments.shipped.size.zero?)) %> - - <% end %> - - <% if can? :update, @order %> - - <% end %> - - <% if can?(:update, @order) && @order.checkout_steps.include?("address") %> - - <% end %> - - <% if can? :update, @order %> - - <% end %> - - <% if can? :index, Spree::Adjustment %> - - <% end %> - - <% if can?(:index, Spree::Payment) %> - - <% end %> - - <% if can? :index, Spree::ReturnAuthorization %> - <% if @order.completed? %> - - <% end %> - <% end %> - - <% if can? :index, Spree::CustomerReturn %> - <% if @order.completed? %> - - <% end %> - <% end %> - - <% if can? :update, @order %> - + <% order_tabs.items.each do |tab| %> + <% next unless (tab.available?(current_ability, @order) && tab.complete?(@order)) %> + <%= content_tag :li, class: 'nav-item', data: { hook: tab.data_hook } do %> + <%= link_to_with_icon( + tab.icon_name, + tab.text, + tab.url(@order), + class: tab.active?(current) ? "active #{tab.classes}" : tab.classes + ) %> + <% end %> <% end %> <% end %> diff --git a/app/views/spree/admin/shared/_product_tabs.html.erb b/app/views/spree/admin/shared/_product_tabs.html.erb index df9baaa7dc..812fd4f123 100644 --- a/app/views/spree/admin/shared/_product_tabs.html.erb +++ b/app/views/spree/admin/shared/_product_tabs.html.erb @@ -3,68 +3,16 @@ <%= @product.name %> <% end %> -<% content_for(:page_tabs) do %> - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'edit.svg', - Spree.t(:details), - edit_admin_product_url(@product), - class: "nav-link #{'active' if current == :details}" %> - - <% end if can?(:admin, Spree::Product) %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'images.svg', - Spree.t(:images), - spree.admin_product_images_url(@product), - class: "nav-link #{'active' if current == :images}" %> - - <% end if can?(:admin, Spree::Image) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'adjust.svg', - Spree.t(:variants), - spree.admin_product_variants_url(@product), - class: "nav-link #{'active' if current == :variants}" %> - - <% end if can?(:admin, Spree::Variant) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'list.svg', - Spree.t(:properties), - spree.admin_product_product_properties_url(@product), - class: "nav-link #{'active' if current == :properties}" %> - - <% end if can?(:admin, Spree::ProductProperty) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'box-seam.svg', - Spree.t(:stock), - stock_admin_product_url(@product), - class: "nav-link #{'active' if current == :stock}" %> - - <% end if can?(:admin, Spree::StockItem) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'currency-exchange.svg', - Spree.t(:prices), - admin_product_prices_path(@product), - class: "nav-link #{'active' if current == :prices}" %> - - <% end if can?(:admin, Spree::Price) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'download.svg', - Spree.t('admin.digitals.digital_assets'), - admin_product_digitals_path(@product), - class: "nav-link #{'active' if current == :digitals} #{current}" %> - - <% end if can?(:admin, Spree::Digital) && !@product.deleted? %> - - <%= content_tag :li, class: 'nav-item' do %> - <%= link_to_with_icon 'translate.svg', - Spree.t(:translations), - translations_admin_product_path(@product), - class: "nav-link #{'active' if current == :translations}" %> - - <% end if can?(:admin, Spree::Product) && !@product.deleted? %> +<% content_for :page_tabs do %> + <% product_tabs.items.each do |tab| %> + <% next unless tab.available?(current_ability, @product) %> + + <% end %> <% end %> diff --git a/app/views/spree/admin/users/_tabs.html.erb b/app/views/spree/admin/users/_tabs.html.erb index ce2832dc36..19b961e879 100644 --- a/app/views/spree/admin/users/_tabs.html.erb +++ b/app/views/spree/admin/users/_tabs.html.erb @@ -4,34 +4,15 @@ <% end %> <% content_for :page_tabs do %> - - - - - + <% user_tabs.items.each do |tab| %> + <% next unless tab.available?(current_ability, @user) %> + + <% end %> <% end %> diff --git a/lib/spree/backend/engine.rb b/lib/spree/backend/engine.rb index 9b4aa681f4..3b59f3e2e1 100644 --- a/lib/spree/backend/engine.rb +++ b/lib/spree/backend/engine.rb @@ -3,7 +3,7 @@ module Spree module Backend class Engine < ::Rails::Engine - Environment = Struct.new(:main_menu) + Environment = Struct.new(:main_menu, :tabs) config.middleware.use 'Spree::Backend::Middleware::SeoAssist' @@ -27,6 +27,10 @@ class Engine < ::Rails::Engine config.after_initialize do Rails.application.reload_routes! Rails.application.config.spree_backend.main_menu = Spree::Admin::MainMenu::DefaultConfigurationBuilder.new.build + Rails.application.config.spree_backend.tabs = {} + Rails.application.config.spree_backend.tabs[:order] = Spree::Admin::Tabs::OrderDefaultTabsBuilder.new.build + Rails.application.config.spree_backend.tabs[:user] = Spree::Admin::Tabs::UserDefaultTabsBuilder.new.build + Rails.application.config.spree_backend.tabs[:product] = Spree::Admin::Tabs::ProductDefaultTabsBuilder.new.build end end end diff --git a/spec/features/admin/products/edit/products_spec.rb b/spec/features/admin/products/edit/products_spec.rb index 33b015387c..dc1a7f5306 100644 --- a/spec/features/admin/products/edit/products_spec.rb +++ b/spec/features/admin/products/edit/products_spec.rb @@ -19,7 +19,7 @@ end end - context 'editing a product with WYSIWYG editer enabled' do + context 'editing a product with WYSIWYG editor enabled' do before do Spree::Backend::Config.product_wysiwyg_editor_enabled = true visit spree.admin_product_path(product) diff --git a/spec/models/spree/admin/main_menu/root_spec.rb b/spec/models/spree/admin/main_menu/root_spec.rb index bb40cf7b36..9790f11a7e 100644 --- a/spec/models/spree/admin/main_menu/root_spec.rb +++ b/spec/models/spree/admin/main_menu/root_spec.rb @@ -4,6 +4,13 @@ module Spree module Admin describe MainMenu::Root, type: :model do let(:root) { described_class.new } + let(:items) { [] } + + before do + items.each { |i| root.add(i) } + end + + it_behaves_like 'implements item manipulation and query methods' describe '#key' do subject { root.key } @@ -40,21 +47,21 @@ module Admin end end - describe '#children?' do - subject { root.children? } + describe '#add_to_section' do + subject { root.add_to_section(section_key, item) } - context 'when there are child items' do - before { root.add(double(key: 'test')) } + let(:items) { [section] } + let(:section) { double(key: section_key) } + let(:section_key) { 'section' } + let(:item) { double(key: 'test') } - it 'returns true' do - expect(subject).to be(true) - end + before do + allow(section).to receive(:add).with(item) end - context 'when there are no child items' do - it 'returns false' do - expect(subject).to be(false) - end + it 'calls add on section' do + expect(section).to receive(:add).with(item) + subject end end end diff --git a/spec/models/spree/admin/main_menu/section_spec.rb b/spec/models/spree/admin/main_menu/section_spec.rb index b555d262cf..a62b00c0c0 100644 --- a/spec/models/spree/admin/main_menu/section_spec.rb +++ b/spec/models/spree/admin/main_menu/section_spec.rb @@ -10,6 +10,8 @@ module Admin let(:availability_check) { nil } let(:items) { [] } + it_behaves_like 'implements item manipulation and query methods' + describe '#key' do subject { section.key } @@ -82,169 +84,6 @@ module Admin end end end - - describe '#add' do - subject { section.add(item) } - let(:item) { double(key: 'test') } - - context 'when there are no child items' do - let(:items) { [] } - - it 'adds the item to the list' do - subject - expect(section.items.count).to eq(1) - end - end - - context 'when there already exists an item with the same key' do - let(:items) { [double(key: 'test')] } - - it 'raises an error' do - expect { subject }.to raise_error(KeyError) - end - end - - context 'when there are other items with different keys' do - let(:items) { [double(key: 'other-key'), double(key: 'different-key') ]} - end - - it 'adds the item to the list' do - subject - expect(section.items.count).to eq(1) - expect(section.items.last.key).to eq('test') - end - end - - describe '#remove' do - subject { section.remove(key) } - let(:key) { 'test-key' } - let(:other_key) { 'other-key' } - - context 'when the element is present in an array' do - let(:items) { [double(key: key), double(key: other_key)] } - - it 'removes the item' do - subject - expect(items.count).to eq(1) - expect(items.first.key).to eq(other_key) - end - end - - context 'when the element is not present in an array' do - let(:items) { [double(key: other_key)] } - - it 'removes the item' do - expect { subject }.to raise_error(KeyError) - end - end - end - - describe '#child_with_key?' do - subject { section.child_with_key?(key) } - let(:key) { 'key' } - - context 'when an item with given key exists' do - let(:items) { [double(key: key), double(key: 'other-key')] } - - it 'returns true' do - expect(subject).to be(true) - end - end - - context 'when an item with given key does not exist' do - let(:items) { [double(key: 'other-key')] } - - it 'returns false' do - expect(subject).to be(false) - end - end - end - - describe '#item_for_key' do - subject { section.item_for_key(key) } - let(:key) { 'key' } - - context 'when an item with given key exists' do - let(:items) { [double(key: 'other-key'), item] } - let(:item) { double(key: key) } - - it 'returns the item' do - expect(subject).to be(item) - end - end - - context 'when an item with given key does not exist' do - let(:items) { [double(key: 'other-key')] } - - it 'returns nil' do - expect(subject).to be(nil) - end - end - end - - describe '#insert_before' do - subject { section.insert_before(existing_key, item) } - - let(:item) { double(key: inserted_key) } - let(:inserted_key) { 'test-item' } - let(:existing_key) { 'test-old' } - - context 'when the list is empty' do - it 'raises an error' do - expect { subject }.to raise_error(KeyError) - end - end - - context 'when an item with specified key does not exist' do - let(:items) { [double(key: 'other-key'), double(key: 'other-key2')] } - - it 'raises an error' do - expect { subject }.to raise_error(KeyError) - end - end - - context 'when an item with specified key exists' do - let(:items) { [double(key: 'other-key'), double(key: existing_key)] } - - it 'inserts the item before the other item' do - subject - expect(section.items.count).to eq(3) - expect(section.items[1].key).to eq(inserted_key) - end - end - end - - describe '#insert_after' do - subject { section.insert_after(existing_key, item) } - - let(:item) { double(key: inserted_key) } - let(:inserted_key) { 'test-item' } - let(:existing_key) { 'test-old' } - - context 'when the list is empty' do - it 'raises an error' do - expect { subject }.to raise_error(KeyError) - end - end - - context 'when an item with specified key does not exist' do - let(:items) { [double(key: 'other-key'), double(key: 'other-key2')] } - - it 'raises an error' do - expect { subject }.to raise_error(KeyError) - end - end - - context 'when an item with specified key exists' do - let(:items) { [double(key: existing_key), double(key: 'other-key')] } - - it 'inserts the item after the other item' do - subject - expect(section.items.count).to eq(3) - expect(section.items[1].key).to eq(inserted_key) - end - end - end end end end diff --git a/spec/models/spree/admin/tabs/order_default_tabs_builder_spec.rb b/spec/models/spree/admin/tabs/order_default_tabs_builder_spec.rb new file mode 100644 index 0000000000..d500d93420 --- /dev/null +++ b/spec/models/spree/admin/tabs/order_default_tabs_builder_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module Spree + module Admin + describe Tabs::OrderDefaultTabsBuilder, type: :model do + let(:builder) { described_class.new } + let(:default_tabs) do + %i(cart + channel + customer + shipments + adjustments + payments + return_authorizations + customer_returns + state_changes) + end + + describe '#build' do + subject { builder.build } + + it 'builds default tabs' do + expect(subject.items.map(&:key)).to match(default_tabs) + end + end + end + end +end diff --git a/spec/models/spree/admin/tabs/product_default_tabs_builder_spec.rb b/spec/models/spree/admin/tabs/product_default_tabs_builder_spec.rb new file mode 100644 index 0000000000..12bfd23d60 --- /dev/null +++ b/spec/models/spree/admin/tabs/product_default_tabs_builder_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +module Spree + module Admin + describe Tabs::ProductDefaultTabsBuilder, type: :model do + let(:builder) { described_class.new } + let(:default_tabs) do + [:details, + :images, + :variants, + :properties, + :stock, + :prices, + 'admin.digitals.digital_assets', + :translations] + end + + describe '#build' do + subject { builder.build } + + it 'builds default tabs' do + expect(subject.items.map(&:key)).to match(default_tabs) + end + end + end + end +end diff --git a/spec/models/spree/admin/tabs/root_spec.rb b/spec/models/spree/admin/tabs/root_spec.rb new file mode 100644 index 0000000000..9057af6443 --- /dev/null +++ b/spec/models/spree/admin/tabs/root_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +module Spree + module Admin + describe Tabs::Root, type: :model do + let(:root) { described_class.new } + let(:items) { [] } + + before do + items.each { |i| root.add(i) } + end + + it_behaves_like 'implements item manipulation and query methods' + end + end +end diff --git a/spec/models/spree/admin/tabs/tab_spec.rb b/spec/models/spree/admin/tabs/tab_spec.rb new file mode 100644 index 0000000000..7d1c4170f1 --- /dev/null +++ b/spec/models/spree/admin/tabs/tab_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +module Spree + module Admin + describe Tabs::Tab, type: :model do + let(:tab) { described_class.new(config) } + let(:config) do + { + icon_name: 'cart-check.svg', + key: 'Cart', + url: '/cart', + classes: 'nav-link', + partial_name: :cart, + availability_check: check, + active_check: check, + completed_check: check, + text: 'Cart', + data_hook: 'data_hook' + } + end + let(:check) { nil } + + describe '#icon_name' do + subject { tab.icon_name } + + it 'returns icon_name' do + expect(subject).to eq(config[:icon_name]) + end + end + + describe '#key' do + subject { tab.key } + + it 'returns key' do + expect(subject).to eq(config[:key]) + end + end + + describe '#url' do + subject { tab.url } + + it 'returns url' do + expect(subject).to eq(config[:url]) + end + end + + describe '#classes' do + subject { tab.classes } + + it 'returns classes' do + expect(subject).to eq(config[:classes]) + end + end + + describe '#text' do + subject { tab.text } + + it 'returns text' do + expect(subject).to eq(config[:text]) + end + end + + describe '#data_hook' do + subject { tab.data_hook } + + it 'returns classes' do + expect(subject).to eq(config[:data_hook]) + end + end + + describe '#available?' do + subject { tab.available?(ability, resource) } + + let(:ability) { double } + let(:resource) { double } + + context 'when availability check is not set' do + it 'is returns true' do + expect(subject).to be(true) + end + end + + context 'when availability check returns false' do + let(:check) { ->(_ability, _resource) { false } } + + it 'returns false' do + expect(subject).to be(false) + end + end + end + + describe '#active?' do + subject { tab.active?(current_tab) } + + context 'when active check returns true' do + let(:check) { ->(_current_tab, _text) { true } } + let(:current_tab) { config[:partial_name] } + + it 'returns true' do + expect(subject).to be(true) + end + end + + context 'when active check returns false' do + let(:check) { ->(_current_tab, _text) { false } } + let(:current_tab) { 'non-matching' } + + it 'returns false' do + expect(subject).to be(false) + end + end + end + + describe '#complete?' do + subject { tab.complete?(resource) } + let(:resource) { double } + + context 'when complete check is not set' do + it 'returns true' do + expect(subject).to be(true) + end + end + + context 'when complete check returns false' do + let(:check) { ->(_resource) { false } } + + it 'returns false' do + expect(subject).to be(false) + end + end + end + end + end +end diff --git a/spec/models/spree/admin/tabs/user_default_tabs_builder_spec.rb b/spec/models/spree/admin/tabs/user_default_tabs_builder_spec.rb new file mode 100644 index 0000000000..52ab2a47cf --- /dev/null +++ b/spec/models/spree/admin/tabs/user_default_tabs_builder_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +module Spree + module Admin + describe Tabs::UserDefaultTabsBuilder, type: :model do + let(:builder) { described_class.new } + let(:default_tabs) do + ['admin.user.account', + 'admin.user.addresses', + 'admin.user.orders', + 'admin.user.items', + 'admin.user.store_credits'] + end + + describe '#build' do + subject { builder.build } + + it 'builds default tabs' do + expect(subject.items.map(&:key)).to match(default_tabs) + end + end + end + end +end diff --git a/spec/support/admin/manipulation_and_query_methods.rb b/spec/support/admin/manipulation_and_query_methods.rb new file mode 100644 index 0000000000..72479794d3 --- /dev/null +++ b/spec/support/admin/manipulation_and_query_methods.rb @@ -0,0 +1,153 @@ +RSpec.shared_examples "implements item manipulation and query methods" do + describe '#add' do + subject { root.add(item) } + let(:item) { double(key: 'test') } + + context "when there's no item with a particular key" do + + it 'appends an item' do + subject + expect(root.items).to include(item) + end + end + + context 'when there is an item with a particular key' do + let(:items) { [item] } + + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + end + + describe '#child_with_key?' do + subject { root.child_with_key?(key) } + let(:key) { 'key' } + + context 'when an item with given key exists' do + let(:items) { [double(key: key), double(key: 'other-key')] } + + it 'returns true' do + expect(subject).to be(true) + end + end + + context 'when an item with given key does not exist' do + let(:items) { [double(key: 'other-key')] } + + it 'returns false' do + expect(subject).to be(false) + end + end + end + + describe '#remove' do + subject { root.remove(key) } + let(:key) { 'key' } + let(:other_key) { 'other-key' } + + context 'when an item with given key exists' do + let(:items) { [double(key: key), double(key: other_key)] } + + it 'removes the item' do + subject + expect(root.items.count).to eq(1) + expect(root.items.first.key).to eq(other_key) + end + end + + context 'when an item with given key does not exist' do + let(:items) { [double(key: 'other-key')] } + + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + end + + describe '#item_for_key' do + subject { root.item_for_key(key) } + let(:key) { 'key' } + + context 'when an item with given key exists' do + let(:items) { [double(key: 'other-key'), item] } + let(:item) { double(key: key) } + + it 'returns the item' do + expect(subject).to be(item) + end + end + + context 'when an item with given key does not exist' do + let(:items) { [double(key: 'other-key')] } + + it 'returns nil' do + expect(subject).to be(nil) + end + end + end + + describe '#insert_before' do + subject { root.insert_before(existing_key, item) } + + let(:item) { double(key: inserted_key) } + let(:inserted_key) { 'test-item' } + let(:existing_key) { 'test-old' } + + context 'when the list is empty' do + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + + context 'when an item with specified key does not exist' do + let(:items) { [double(key: 'other-key'), double(key: 'other-key2')] } + + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + + context 'when an item with specified key exists' do + let(:items) { [double(key: 'other-key'), double(key: existing_key)] } + + it 'inserts the item before the other item' do + subject + expect(root.items.count).to eq(3) + expect(root.items[1].key).to eq(inserted_key) + end + end + end + + describe '#insert_after' do + subject { root.insert_after(existing_key, item) } + + let(:item) { double(key: inserted_key) } + let(:inserted_key) { 'test-item' } + let(:existing_key) { 'test-old' } + + context 'when the list is empty' do + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + + context 'when an item with specified key does not exist' do + let(:items) { [double(key: 'other-key'), double(key: 'other-key2')] } + + it 'raises an error' do + expect { subject }.to raise_error(KeyError) + end + end + + context 'when an item with specified key exists' do + let(:items) { [double(key: existing_key), double(key: 'other-key')] } + + it 'inserts the item after the other item' do + subject + expect(root.items.count).to eq(3) + expect(root.items[1].key).to eq(inserted_key) + end + end + end +end \ No newline at end of file