Skip to content

Commit

Permalink
Merge pull request #5532 from nebulab/rainerd/admin/add-stock-items-i…
Browse files Browse the repository at this point in the history
…ndex-page

[Admin] Add `Stock Items` index component
  • Loading branch information
rainerdema authored Dec 5, 2023
2 parents 8a41475 + 349502f commit 946a49e
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def filters
]
end
},

{
presentation: t('.filters.shipment_state'),
combinator: 'or',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<%= page do %>
<%= page_header do %>
<%= page_header_title title %>
<% end %>

<%= render component('ui/table').new(
id: stimulus_id,
data: {
class: Spree::StockItem,
rows: @page.records,
prev: prev_page_path,
next: next_page_path,
columns: columns,
batch_actions: batch_actions,
},
search: {
name: :q,
value: params[:q],
url: solidus_admin.stock_items_path,
searchbar_key: :variant_product_name_or_variant_sku_or_variant_option_values_name_or_variant_option_values_presentation_cont,
filters: filters,
scopes: scopes,
},
) %>
<% end %>
153 changes: 153 additions & 0 deletions admin/app/components/solidus_admin/stock_items/index/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# frozen_string_literal: true

class SolidusAdmin::StockItems::Index::Component < SolidusAdmin::BaseComponent
include SolidusAdmin::Layout::PageHelpers

def initialize(page:)
@page = page
end

def title
Spree::StockItem.model_name.human.pluralize
end

def prev_page_path
solidus_admin.url_for(**request.params, page: @page.number - 1, only_path: true) unless @page.first?
end

def next_page_path
solidus_admin.url_for(**request.params, page: @page.next_param, only_path: true) unless @page.last?
end

def batch_actions
[]
end

def scopes
[
{ label: t('.scopes.all_stock_items'), name: 'all', default: true },
{ label: t('.scopes.back_orderable'), name: 'back_orderable' },
{ label: t('.scopes.out_of_stock'), name: 'out_of_stock' },
{ label: t('.scopes.low_stock'), name: 'low_stock' },
{ label: t('.scopes.in_stock'), name: 'in_stock' },
]
end

def filters
[
{
presentation: t('.filters.stock_locations'),
combinator: 'or',
attribute: "stock_location_id",
predicate: "eq",
options: Spree::StockLocation.all.map do |stock_location|
[
stock_location.name.titleize,
stock_location.id
]
end
},
{
presentation: t('.filters.variants'),
combinator: 'or',
attribute: "variant_id",
predicate: "eq",
options: Spree::Variant.all.map do |variant|
[
variant.descriptive_name,
variant.id
]
end
},
]
end

def columns
[
image_column,
name_column,
sku_column,
variant_column,
stock_location_column,
back_orderable_column,
count_on_hand_column,
]
end

def image_column
{
col: { class: "w-[72px]" },
header: tag.span('aria-label': t('.image'), role: 'text'),
data: ->(stock_item) do
image = stock_item.variant.gallery.images.first or return

render(
component('ui/thumbnail').new(
src: image.url(:small),
alt: stock_item.variant.name
)
)
end
}
end

def name_column
{
header: :name,
data: ->(stock_item) do
content_tag :div, stock_item.variant.name
end
}
end

def sku_column
{
header: :sku,
data: ->(stock_item) do
content_tag :div, stock_item.variant.sku
end
}
end

def variant_column
{
header: :variant,
data: ->(stock_item) do
content_tag(:div, class: "space-y-0.5") do
safe_join(
stock_item.variant.option_values.sort_by(&:option_type_name).map do |option_value|
render(component('ui/badge').new(name: "#{option_value.option_type_presentation}: #{option_value.presentation}"))
end
)
end
end
}
end

def stock_location_column
{
header: :stock_location,
data: ->(stock_item) do
link_to stock_item.stock_location.name, spree.admin_stock_location_stock_movements_path(stock_item.stock_location.id, q: { variant_sku_eq: stock_item.variant.sku })
end
}
end

def back_orderable_column
{
header: :back_orderable,
data: ->(stock_item) do
stock_item.backorderable? ? component('ui/badge').yes : component('ui/badge').no
end
}
end

def count_on_hand_column
{
header: :count_on_hand,
data: ->(stock_item) do
content_tag :div, stock_item.count_on_hand
end
}
end
end
10 changes: 10 additions & 0 deletions admin/app/components/solidus_admin/stock_items/index/component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
en:
filters:
stock_locations: Stock Locations
variants: Variants
scopes:
all_stock_items: All
back_orderable: Back Orderable
out_of_stock: Out Of Stock
low_stock: Low Stock
in_stock: In Stock
5 changes: 3 additions & 2 deletions admin/app/components/solidus_admin/ui/table/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ export default class extends Controller {
}
}

showSearch(event) {
showSearch({ detail: { avoidFocus } }) {
this.modeValue = "search"
this.render()
this.searchFieldTarget.focus()

if (!avoidFocus) this.searchFieldTarget.focus()
}

search() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export default class extends Controller {
}

showSearch() {
if (this.isAnyCheckboxChecked())
this.dispatch("showSearch")
if (this.isAnyCheckboxChecked()) {
this.dispatch("showSearch", { detail: { avoidFocus: true } })
}
}

filterOptions(event) {
Expand Down
26 changes: 26 additions & 0 deletions admin/app/controllers/solidus_admin/stock_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module SolidusAdmin
class StockItemsController < SolidusAdmin::BaseController
include SolidusAdmin::ControllerHelpers::Search

search_scope(:all, default: true) { _1 }
search_scope(:back_orderable) { _1.where(backorderable: true) }
search_scope(:out_of_stock) { _1.where('count_on_hand <= 0') }
search_scope(:low_stock) { _1.where('count_on_hand > 0 AND count_on_hand < ?', SolidusAdmin::Config[:low_stock_value]) }
search_scope(:in_stock) { _1.where('count_on_hand > 0') }

def index
stock_items = apply_search_to(
Spree::StockItem.order(created_at: :desc, id: :desc),
param: :q,
)

set_page_and_extract_portion_from(stock_items)

respond_to do |format|
format.html { render component('stock_items/index').new(page: @page) }
end
end
end
end
4 changes: 4 additions & 0 deletions admin/config/locales/stock_items.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
en:
solidus_admin:
stock_items:
title: "Stock Items"
1 change: 1 addition & 0 deletions admin/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@
admin_resources :tax_categories, only: [:index, :destroy]
admin_resources :tax_rates, only: [:index, :destroy]
admin_resources :payment_methods, only: [:index, :destroy], sortable: true
admin_resources :stock_items, only: [:index]
end
6 changes: 6 additions & 0 deletions admin/lib/solidus_admin/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ class Configuration < Spree::Preferences::Configuration
# meaning it will search by product name or product variants sku.
preference :product_search_key, :string, default: :name_or_variants_including_master_sku_cont

# @!attribute [rw] low_stock_value
# @return [Integer] The low stock value determines the threshold at which products are considered low in stock.
# Products with a count_on_hand less than or equal to this value will be considered low in stock.
# Default: 10
preference :low_stock_value, :integer, default: 10

preference :storefront_product_path_proc, :proc, default: ->(_version) {
->(product) { "/products/#{product.slug}" }
}
Expand Down
56 changes: 56 additions & 0 deletions admin/spec/features/stock_items_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'spec_helper'

describe "Stock Items", :js, type: :feature do
before { sign_in create(:admin_user, email: '[email protected]') }

it "lists stock items and allows navigating through scopes" do
non_backorderable = create(:stock_item, backorderable: false)
backorderable = create(:stock_item, backorderable: true)
out_of_stock = begin
item = create(:stock_item, backorderable: false)
item.reduce_count_on_hand_to_zero
item
end
low_stock = begin
item = create(:stock_item, backorderable: false)
item.set_count_on_hand(SolidusAdmin::Config[:low_stock_value] - 1)
item
end

visit "/admin/stock_items"

# `All` default scope
expect(page).to have_content(non_backorderable.variant.sku)
expect(page).to have_content(backorderable.variant.sku)
expect(page).to have_content(out_of_stock.variant.sku)
expect(page).to have_content(low_stock.variant.sku)

click_on 'Back Orderable'
expect(page).to_not have_content(non_backorderable.variant.sku)
expect(page).to have_content(backorderable.variant.sku)
expect(page).to_not have_content(out_of_stock.variant.sku)
expect(page).to_not have_content(low_stock.variant.sku)

click_on 'Out Of Stock'
expect(page).to_not have_content(non_backorderable.variant.sku)
expect(page).to_not have_content(backorderable.variant.sku)
expect(page).to have_content(out_of_stock.variant.sku)
expect(page).to_not have_content(low_stock.variant.sku)

click_on 'Low Stock'
expect(page).to_not have_content(non_backorderable.variant.sku)
expect(page).to_not have_content(backorderable.variant.sku)
expect(page).to_not have_content(out_of_stock.variant.sku)
expect(page).to have_content(low_stock.variant.sku)

click_on 'In Stock'
expect(page).to have_content(non_backorderable.variant.sku)
expect(page).to have_content(backorderable.variant.sku)
expect(page).to_not have_content(out_of_stock.variant.sku)
expect(page).to have_content(low_stock.variant.sku)

expect(page).to be_axe_clean
end
end
1 change: 1 addition & 0 deletions core/app/models/spree/stock_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class StockItem < Spree::Base
after_touch { variant.touch }

self.allowed_ransackable_attributes = ['count_on_hand', 'stock_location_id']
self.allowed_ransackable_associations = %w[variant]

# @return [Array<Spree::InventoryUnit>] the backordered inventory units
# associated with this stock item
Expand Down

0 comments on commit 946a49e

Please sign in to comment.