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?)) %>
-
- <%= link_to_with_icon 'cart-check.svg',
- Spree.t(:cart),
- cart_admin_order_url(@order),
- class: "#{'active' if current == :cart} nav-link" %>
-
- <% end %>
-
- <% if can? :update, @order %>
-
- <%= link_to_with_icon 'funnel.svg',
- Spree.t(:channel),
- channel_admin_order_url(@order),
- class: "#{'active' if current == :channel} nav-link" %>
-
- <% end %>
-
- <% if can?(:update, @order) && @order.checkout_steps.include?("address") %>
-
- <%= link_to_with_icon 'person-lines-fill.svg',
- Spree.t(:customer),
- spree.admin_order_customer_url(@order),
- class: "#{'active' if current == :customer_details} nav-link" %>
-
- <% end %>
-
- <% if can? :update, @order %>
-
- <%= link_to_with_icon 'truck.svg',
- Spree.t(:shipments),
- edit_admin_order_url(@order),
- class: "#{'active' if current == :shipments} nav-link" %>
-
- <% end %>
-
- <% if can? :index, Spree::Adjustment %>
-
- <%= link_to_with_icon 'adjust.svg',
- Spree.t(:adjustments),
- spree.admin_order_adjustments_url(@order),
- class: "#{'active' if current == :adjustments} nav-link" %>
-
- <% end %>
-
- <% if can?(:index, Spree::Payment) %>
-
- <%= link_to_with_icon 'credit-card.svg',
- Spree.t(:payments),
- spree.admin_order_payments_url(@order),
- class: "#{'active' if current == :payments} nav-link" %>
-
- <% end %>
-
- <% if can? :index, Spree::ReturnAuthorization %>
- <% if @order.completed? %>
-
- <%= link_to_with_icon 'enter.svg',
- Spree.t(:return_authorizations),
- spree.admin_order_return_authorizations_url(@order),
- class: "#{'active' if current == :return_authorizations} nav-link" %>
-
- <% end %>
- <% end %>
-
- <% if can? :index, Spree::CustomerReturn %>
- <% if @order.completed? %>
-
- <%= link_to_with_icon 'returns.svg',
- Spree.t(:customer_returns),
- spree.admin_order_customer_returns_url(@order),
- class: "#{'active' if current == :customer_returns} nav-link" %>
-
- <% end %>
- <% end %>
-
- <% if can? :update, @order %>
-
- <%= link_to_with_icon 'calendar3.svg',
- Spree::StateChange.human_attribute_name(:state_changes),
- spree.admin_order_state_changes_url(@order),
- class: "#{'active' if current == :state_changes} nav-link" %>
-
+ <% 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) %>
+
+ <%= link_to_with_icon(
+ tab.icon_name,
+ tab.text,
+ tab.url(@product),
+ class: tab.active?(current) ? "active #{tab.classes}" : tab.classes
+ ) %>
+
+ <% 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 %>
-
- <%= link_to_with_icon 'person.svg',
- Spree.t(:"admin.user.account"),
- spree.edit_admin_user_path(@user),
- class: "nav-link #{'active' if current == :account}" %>
-
-
- <%= link_to_with_icon 'pin-map.svg',
- Spree.t(:"admin.user.addresses"),
- spree.addresses_admin_user_path(@user),
- class: "nav-link #{'active' if current == :address}" %>
-
-
- <%= link_to_with_icon 'inbox.svg',
- Spree.t(:"admin.user.orders"),
- spree.orders_admin_user_path(@user),
- class: "nav-link #{'active' if current == :orders}" %>
-
-
- <%= link_to_with_icon 'tag.svg',
- Spree.t(:"admin.user.items"),
- spree.items_admin_user_path(@user),
- class: "nav-link #{'active' if current == :items}" %>
-
-
- <%= link_to_with_icon 'gift.svg',
- Spree.t(:"admin.user.store_credits"),
- spree.admin_user_store_credits_path(@user),
- class: "nav-link #{'active' if current == :store_credits}" %>
-
+ <% user_tabs.items.each do |tab| %>
+ <% next unless tab.available?(current_ability, @user) %>
+
+ <%= link_to_with_icon(
+ tab.icon_name,
+ tab.text,
+ tab.url(@user),
+ class: tab.active?(current) ? "active #{tab.classes}" : tab.classes
+ ) %>
+
+ <% 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