diff --git a/app/components/product_component.rb b/app/components/product_component.rb index e90eaa33be..5d6ce0c9fc 100644 --- a/app/components/product_component.rb +++ b/app/components/product_component.rb @@ -1,18 +1,26 @@ # frozen_string_literal: true class ProductComponent < ViewComponentReflex::Component + DATETIME_FORMAT = '%F %T' + def initialize(product:, columns:) super @product = product @image = @product.images[0] if product.images.any? - @columns = columns.map { |c| + @columns = columns.map do |c| { id: c[:value], value: column_value(c[:value]) } - } + end end + # This must be define when using ProductComponent.with_collection() + def collection_key + @product.id + end + + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength def column_value(column) case column when 'name' @@ -25,6 +33,27 @@ class ProductComponent < ViewComponentReflex::Component @product.supplier.name when 'category' @product.taxons.map(&:name).join(', ') + when 'sku' + @product.sku + when 'on_hand' + @product.on_hand || 0 + when 'on_demand' + @product.on_demand + when 'tax_category' + @product.tax_category.name + when 'inherits_properties' + @product.inherits_properties + when 'available_on' + format_date(@product.available_on) + when 'import_date' + format_date(@product.import_date) end end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength + + private + + def format_date(date) + date&.strftime(DATETIME_FORMAT) || '' + end end diff --git a/app/components/products_table_component.rb b/app/components/products_table_component.rb index 6d9dc7de65..44e06e8eb1 100644 --- a/app/components/products_table_component.rb +++ b/app/components/products_table_component.rb @@ -3,27 +3,37 @@ class ProductsTableComponent < ViewComponentReflex::Component include Pagy::Backend - SORTABLE_COLUMNS = ["name"].freeze - SELECTABLE_COMUMNS = [{ label: I18n.t("admin.products_page.columns_selector.price"), - value: "price" }, - { label: I18n.t("admin.products_page.columns_selector.unit"), - value: "unit" }, - { label: I18n.t("admin.products_page.columns_selector.producer"), - value: "producer" }, - { label: I18n.t("admin.products_page.columns_selector.category"), - value: "category" }].sort { |a, b| + SORTABLE_COLUMNS = ['name', 'import_date'].freeze + SELECTABLE_COLUMNS = [ + { label: I18n.t("admin.products_page.columns_selector.price"), value: "price" }, + { label: I18n.t("admin.products_page.columns_selector.unit"), value: "unit" }, + { label: I18n.t("admin.products_page.columns_selector.producer"), value: "producer" }, + { label: I18n.t("admin.products_page.columns_selector.category"), value: "category" }, + { label: I18n.t("admin.products_page.columns_selector.sku"), value: "sku" }, + { label: I18n.t("admin.products_page.columns_selector.on_hand"), value: "on_hand" }, + { label: I18n.t("admin.products_page.columns_selector.on_demand"), value: "on_demand" }, + { label: I18n.t("admin.products_page.columns_selector.tax_category"), value: "tax_category" }, + { + label: I18n.t("admin.products_page.columns_selector.inherits_properties"), + value: "inherits_properties" + }, + { label: I18n.t("admin.products_page.columns_selector.available_on"), value: "available_on" }, + { label: I18n.t("admin.products_page.columns_selector.import_date"), value: "import_date" } + ].sort do |a, b| a[:label] <=> b[:label] - }.freeze + end.freeze + PER_PAGE_VALUE = [10, 25, 50, 100].freeze PER_PAGE = PER_PAGE_VALUE.map { |value| { label: value, value: value } } - NAME_COLUMN = { label: I18n.t("admin.products_page.columns.name"), value: "name", - sortable: true }.freeze + NAME_COLUMN = { + label: I18n.t("admin.products_page.columns.name"), value: "name", sortable: true + }.freeze def initialize(user:) super @user = user - @selectable_columns = SELECTABLE_COMUMNS - @columns_selected = ["price", "unit"] + @selectable_columns = SELECTABLE_COLUMNS + @columns_selected = ['unit', 'price', 'on_hand', 'category', 'import_date'] @per_page = PER_PAGE @per_page_selected = [10] @categories = [{ label: "All", value: "all" }] + @@ -40,16 +50,20 @@ class ProductsTableComponent < ViewComponentReflex::Component @search_term = "" end + # any change on a "reflex_data_attributes" (defined in the template) will trigger a re render def before_render fetch_products refresh_columns end + # Element refers to the component the data is set on def search_term + # Element is SearchInputComponent @search_term = element.dataset['value'] end def toggle_column + # Element is SelectorComponent column = element.dataset['value'] @columns_selected = if @columns_selected.include?(column) @columns_selected - [column] @@ -59,26 +73,33 @@ class ProductsTableComponent < ViewComponentReflex::Component end def click_sort - @sort = { column: element.dataset['sort-value'], - direction: element.dataset['sort-direction'] == "asc" ? "desc" : "asc" } + # Element is TableHeaderComponent + @sort = { + column: element.dataset['sort-value'], + direction: element.dataset['sort-direction'] == "asc" ? "desc" : "asc" + } end def toggle_per_page + # Element is SelectorComponent selected = element.dataset['value'].to_i @per_page_selected = [selected] if PER_PAGE_VALUE.include?(selected) end def toggle_category + # Element is SelectorWithFilterComponent category_clicked = element.dataset['value'] @categories_selected = toggle_selector_with_filter(category_clicked, @categories_selected) end def toggle_producer + # Element is SelectorWithFilterComponent producer_clicked = element.dataset['value'] @producers_selected = toggle_selector_with_filter(producer_clicked, @producers_selected) end def change_page + # Element is PaginationComponent page = element.dataset['page'].to_i @page = page if page > 0 end @@ -86,10 +107,13 @@ class ProductsTableComponent < ViewComponentReflex::Component private def refresh_columns - @columns = @columns_selected.map { |column| - { label: I18n.t("admin.products_page.columns.#{column}"), value: column, - sortable: SORTABLE_COLUMNS.include?(column) } - }.sort! { |a, b| a[:label] <=> b[:label] } + @columns = @columns_selected.map do |column| + { + label: I18n.t("admin.products_page.columns.#{column}"), + value: column, + sortable: SORTABLE_COLUMNS.include?(column) + } + end.sort! { |a, b| a[:label] <=> b[:label] } @columns.unshift(NAME_COLUMN) end @@ -145,8 +169,13 @@ class ProductsTableComponent < ViewComponentReflex::Component def product_query_includes [ master: [:images], - variants: [:default_price, :stock_locations, :stock_items, :variant_overrides, - { option_values: :option_type }] + variants: [ + :default_price, + :stock_locations, + :stock_items, + :variant_overrides, + { option_values: :option_type } + ] ] end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 49b7506aef..106d7ed76e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -492,11 +492,25 @@ en: price: Price producer: Producer category: Category + sku: SKU + on_hand: "On Hand" + on_demand: "On Demand" + tax_category: "Tax Category" + inherits_properties: "Inherits Properties?" + available_on: "Available On" + import_date: "Import Date" columns_selector: unit: Unit price: Price producer: Producer category: Category + sku: SKU + on_hand: "On Hand" + on_demand: "On Demand" + tax_category: "Tax Category" + inherits_properties: "Inherits Properties?" + available_on: "Available On" + import_date: "Import Date" adjustments: skipped_changing_canceled_order: "You can't change a cancelled order." # Common properties / models diff --git a/spec/components/product_component_spec.rb b/spec/components/product_component_spec.rb new file mode 100644 index 0000000000..540f4befef --- /dev/null +++ b/spec/components/product_component_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe ProductComponent, type: :component do + let(:product) { create(:simple_product) } + + describe 'unit' do + before do + render_inline( + ProductComponent.new( + product: product, columns: [{ label: "Unit", value: "unit", sortable: false }] + ) + ) + end + + it 'concatenates the unit value and the unit description' do + expect(page.find('.unit')).to have_content '1.0 weight' + end + end + + describe 'category' do + let(:product) do + product = create(:simple_product) + product.taxons = taxons + + product + end + let(:taxons) { [create(:taxon, name: 'random 1'), create(:taxon, name: 'random 2')] } + + before do + render_inline( + ProductComponent.new( + product: product, columns: [{ label: "Category", value: "category", sortable: false }] + ) + ) + end + + it "joins the categories' name" do + expect(page.find('.category')).to have_content( + /random 1, random 2/, exact: true, normalize_ws: true + ) + end + end + + describe 'on_hand' do + let(:product) { create(:simple_product, on_hand: on_hand) } + let(:on_hand) { 5 } + + before do + render_inline( + ProductComponent.new( + product: product, columns: [{ label: "On Hand", value: "on_hand", sortable: false }] + ) + ) + end + + it 'returns product on_hand' do + expect(page.find('.on_hand')).to have_content(on_hand) + end + + context 'when on_hand is nil' do + let(:on_hand) { nil } + + it 'returns 0' do + expect(page.find('.on_hand')).to have_content(0.to_s) + end + end + end + + # This also covers import_date + describe 'available_on' do + let(:product) { create(:simple_product, available_on: available_on) } + let(:available_on) { Time.zone.now } + + before do + render_inline( + ProductComponent.new( + product: product, + columns: [{ label: "Available On", value: "available_on", sortable: false }] + ) + ) + end + + it 'returns formated available_on' do + expect(page.find('.available_on')).to have_content(available_on.strftime('%F %T')) + end + + context 'when available_on is nil' do + let(:available_on) { nil } + + it 'returns an empty string' do + expect(page.find('.available_on')).to have_content('') + end + end + end +end