Merge pull request #12401 from chahmedejaz/task/11060-change-producer-category-tax-category

[BUU] Change Producer, Category and Tax Category
This commit is contained in:
Rachel Arnould
2024-05-07 15:10:14 +02:00
committed by GitHub
15 changed files with 293 additions and 50 deletions

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
class SearchableDropdownComponent < ViewComponent::Base
REMOVED_SEARCH_PLUGIN = { 'tom-select-options-value': '{ "plugins": [] }' }.freeze
MINIMUM_OPTIONS_FOR_SEARCH_FIELD = 11 # at least 11 options are required for the search field
def initialize(
form:,
name:,
options:,
selected_option:,
placeholder_value:,
include_blank: false,
aria_label: ''
)
@f = form
@name = name
@options = options
@selected_option = selected_option
@placeholder_value = placeholder_value
@include_blank = include_blank
@aria_label = aria_label
end
private
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank, :aria_label
def classes
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"
end
def data
{
controller: "tom-select",
'tom-select-placeholder-value': placeholder_value
}.merge(remove_search_plugin? ? REMOVED_SEARCH_PLUGIN : {})
end
def remove_search_plugin?
@remove_search_plugin ||= options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD
end
end

View File

@@ -0,0 +1 @@
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label

View File

@@ -7,7 +7,7 @@ module Admin
def index
fetch_products
render "index", locals: { producers:, categories:, flash: }
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
end
def bulk_update
@@ -24,7 +24,8 @@ module Admin
elsif product_set.errors.present?
@error_counts = { saved: product_set.saved_count, invalid: product_set.invalid.count }
render "index", status: :unprocessable_entity, locals: { producers:, categories:, flash: }
render "index", status: :unprocessable_entity,
locals: { producers:, categories:, tax_category_options:, flash: }
end
end
@@ -59,6 +60,10 @@ module Admin
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
end
def tax_category_options
Spree::TaxCategory.order(:name).pluck(:name, :id)
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(spree_current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result

View File

@@ -89,8 +89,8 @@ class ProductsReflex < ApplicationReflex
html: render(partial: "admin/products_v3/content",
locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, category_id: @category_id,
flashes: flash })
category_options: categories, tax_category_options:,
category_id: @category_id, flashes: flash })
)
cable_ready.replace_state(
@@ -125,6 +125,10 @@ class ProductsReflex < ApplicationReflex
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
end
def tax_category_options
Spree::TaxCategory.order(:name).pluck(:name, :id)
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result(distinct: true)

View File

@@ -15,7 +15,7 @@
.container.results
.sixteen.columns
= render partial: 'sort', locals: { pagy: pagy, search_term: search_term, producer_id: producer_id, category_id: category_id }
= render partial: 'table', locals: { products: products }
= render partial: 'table', locals: { products:, producer_options:, category_options:, tax_category_options: }
- if pagy.present? && pagy.pages > 1
= render partial: 'admin/shared/stimulus_pagination', locals: { pagy: pagy }
- else

View File

@@ -9,7 +9,7 @@
%td.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.multi-field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
%td.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.hidden_field :variant_unit
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,
@@ -27,8 +27,13 @@
-# empty
%td.align-right
-# empty
%td.align-left
.content= product.supplier&.name
%td.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :supplier_id,
aria_label: t('.producer_field_name'),
options: producer_options,
selected_option: product.supplier_id,
placeholder_value: t('admin.products_v3.filters.search_for_producers')))
%td.align-left
-# empty
%td.align-left

View File

@@ -66,17 +66,17 @@
controller: "nested-form product",
action: 'rails-nested-form:add->bulk-form#registerElements' } }
%tr
= render partial: 'product_row', locals: { product:, f: product_form }
= render partial: 'product_row', locals: { f: product_form, product:, producer_options: }
- product.variants.each_with_index do |variant, variant_index|
= form.fields_for("products][#{product_index}][variants_attributes][", variant, index: variant_index) do |variant_form|
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant:, f: variant_form }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options: }
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", product.variants.build) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form, category_options:, tax_category_options: }
%tr{ 'data-nested-form-target': "target" }
%tr.condensed

View File

@@ -40,11 +40,22 @@
= f.check_box :on_demand, 'data-action': 'change->toggle-control#disableIfPresent change->popout#closeIfChecked'
= t(:on_demand)
%td.align-left
.content= variant.product.supplier&.name # same as product
%td.align-left
.content= variant.primary_taxon&.name
%td.align-left
.content= (variant.tax_category_id ? variant.tax_category&.name : t('.none_tax_category')) # TODO: convert to dropdown
-# empty producer name
%td.field.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :primary_taxon_id,
options: category_options,
selected_option: variant.primary_taxon_id,
aria_label: t('.category_field_name'),
placeholder_value: t('admin.products_v3.filters.search_for_categories')))
%td.field.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :tax_category_id,
options: tax_category_options,
selected_option: variant.tax_category_id,
include_blank: t('.none_tax_category'),
aria_label: t('.tax_category_field_name'),
placeholder_value: t('.search_for_tax_categories')))
%td.align-left
-# empty
%td.align-right

View File

@@ -15,7 +15,7 @@
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, category_id: @category_id,
flashes: flash }
tax_category_options:, flashes: flash }
- %w[product variant].each do |object_type|
= render partial: 'delete_modal', locals: { object_type: }
#modal-component

View File

@@ -135,10 +135,17 @@ export default class BulkFormController extends Controller {
if (element.type == "checkbox") {
return element.defaultChecked !== undefined && element.checked != element.defaultChecked;
} else if (element.type == "select-one") {
// (weird) Behavior of select element's include_blank option in Rails:
// If a select field has include_blank option selected (its value will be ''),
// its respective option doesn't have the selected attribute
// but selectedOptions have that option present
const defaultSelected = Array.from(element.options).find((opt) =>
opt.hasAttribute("selected"),
);
return element.selectedOptions[0] != defaultSelected;
const selectedOption = element.selectedOptions[0];
const areBothBlank = selectedOption.value === '' && defaultSelected === undefined
return !areBothBlank && selectedOption !== defaultSelected;
} else {
return element.defaultValue !== undefined && element.value != element.defaultValue;
}

View File

@@ -170,11 +170,10 @@
.field {
padding: 0;
}
.multi-field {
// Allow wrap with small gap
display: flex;
flex-wrap: wrap;
gap: 3px;
.fullwidth + .field {
// Assume wrap, so add small gap
margin-top: 3px;
}
.ts-control {

View File

@@ -910,6 +910,11 @@ en:
error: Unable to delete the variant
variant_row:
none_tax_category: None
search_for_tax_categories: "Search for tax categories"
category_field_name: "Category"
tax_category_field_name: "Tax Category"
product_row:
producer_field_name: "Producer"
product_import:
title: Product Import
file_not_found: File not found or could not be opened

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module TomselectHelper
def tomselect_open(field_name)
page.find("##{field_name}-ts-control").click
end
def tomselect_multiselect(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
tomselect_wrapper.find(:css, '.ts-dropdown.multi .ts-dropdown-content .option',
text: value).click
end
def tomselect_search_and_select(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
# Use send_keys as setting the value directly doesn't trigger the search
tomselect_wrapper.find(:css, '.ts-dropdown input.dropdown-input').send_keys(value)
tomselect_wrapper.find(:css, '.ts-dropdown .ts-dropdown-content .option', text: value).click
end
def tomselect_select(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
tomselect_wrapper.find(:css, '.ts-dropdown .ts-dropdown-content .option', text: value).click
end
def open_tomselect_to_validate!(page, field_name)
tomselect_wrapper = page.find_field(field_name).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click # open the dropdown
raise 'Please pass the block for expectations' unless block_given?
# execute block containing expectations
yield
tomselect_wrapper.find(
'.ts-dropdown .ts-dropdown-content .option.active',
).click # close the dropdown by selecting the already selected value
end
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module WebHelper
include TomselectHelper
def have_input(name, opts = {})
selector = "[name='#{name}']"
selector += "[placeholder='#{opts[:placeholder]}']" if opts.key? :placeholder
@@ -86,32 +88,6 @@ module WebHelper
find(:css, ".select2-result-label", text: options[:select_text] || value).click
end
def tomselect_open(field_name)
page.find("##{field_name}-ts-control").click
end
def tomselect_multiselect(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
tomselect_wrapper.find(:css, '.ts-dropdown.multi .ts-dropdown-content .option',
text: value).click
end
def tomselect_search_and_select(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
# Use send_keys as setting the value directly doesn't trigger the search
tomselect_wrapper.find(:css, '.ts-dropdown input.dropdown-input').send_keys(value)
tomselect_wrapper.find(:css, '.ts-dropdown .ts-dropdown-content .option', text: value).click
end
def tomselect_select(value, options)
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
tomselect_wrapper.find(".ts-control").click
tomselect_wrapper.find(:css, '.ts-dropdown .ts-dropdown-content .option', text: value).click
end
def request_monitor_finished(controller = nil)
page.evaluate_script("#{angular_scope(controller)}.scope().RequestMonitor.loading == false")
end

View File

@@ -14,6 +14,10 @@ describe 'As an enterprise user, I can manage my products', feature: :admin_styl
login_as user
end
let(:producer_search_selector) { 'input[placeholder="Search for producers"]' }
let(:categories_search_selector) { 'input[placeholder="Search for categories"]' }
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
it "can see the new product page" do
visit admin_products_url
expect(page).to have_content "Bulk Edit Products"
@@ -666,6 +670,117 @@ describe 'As an enterprise user, I can manage my products', feature: :admin_styl
end
end
describe "Changing producers, category and tax category" do
let!(:variant_a1) {
product_a.variants.first.tap{ |v|
v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5,
on_demand: false
}
}
let!(:product_a) {
create(:simple_product, name: "Apples", sku: "APL-00",
variant_unit: "weight", variant_unit_scale: 1) # Grams
}
context "when they are under 11" do
before do
create_list(:supplier_enterprise, 9, users: [user])
create_list(:tax_category, 9)
create_list(:taxon, 2)
visit admin_products_url
end
it "should not display search input, change the producers, category and tax category" do
producer_to_select = random_producer(product_a)
category_to_select = random_category(variant_a1)
tax_category_to_select = random_tax_category
within row_containing_name(product_a.name) do
validate_tomselect_without_search!(
page, "Producer",
producer_search_selector
)
tomselect_select(producer_to_select, from: "Producer")
end
within row_containing_name(variant_a1.display_name) do
validate_tomselect_without_search!(
page, "Category",
categories_search_selector
)
tomselect_select(category_to_select, from: "Category")
validate_tomselect_without_search!(
page, "Tax Category",
tax_categories_search_selector
)
tomselect_select(tax_category_to_select, from: "Tax Category")
end
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
variant_a1.reload
expect(product_a.supplier.name).to eq(producer_to_select)
expect(variant_a1.primary_taxon.name).to eq(category_to_select)
expect(variant_a1.tax_category.name).to eq(tax_category_to_select)
end
end
context "when they are over 11" do
before do
create_list(:supplier_enterprise, 11, users: [user])
create_list(:tax_category, 11)
create_list(:taxon, 11)
visit admin_products_url
end
it "should display search input, change the producer" do
producer_to_select = random_producer(product_a)
category_to_select = random_category(variant_a1)
tax_category_to_select = random_tax_category
within row_containing_name(product_a.name) do
validate_tomselect_with_search!(
page, "Producer",
producer_search_selector
)
tomselect_search_and_select(producer_to_select, from: "Producer")
end
within row_containing_name(variant_a1.display_name) do
sleep(0.1)
validate_tomselect_with_search!(
page, "Category",
categories_search_selector
)
tomselect_search_and_select(category_to_select, from: "Category")
sleep(0.1)
validate_tomselect_with_search!(
page, "Tax Category",
tax_categories_search_selector
)
tomselect_search_and_select(tax_category_to_select, from: "Tax Category")
end
click_button "Save changes"
expect(page).to have_content "Changes saved"
product_a.reload
variant_a1.reload
expect(product_a.supplier.name).to eq(producer_to_select)
expect(variant_a1.primary_taxon.name).to eq(category_to_select)
expect(variant_a1.tax_category.name).to eq(tax_category_to_select)
end
end
end
describe "edit image" do
shared_examples "updating image" do
it "saves product image" do
@@ -1030,6 +1145,35 @@ describe 'As an enterprise user, I can manage my products', feature: :admin_styl
end
def tax_category_column
@tax_category_column ||= 'td:nth-child(10)'
@tax_category_column ||= '[data-controller="variant"] > td:nth-child(10)'
end
def validate_tomselect_without_search!(page, field_name, search_selector)
open_tomselect_to_validate!(page, field_name) do
expect(page).not_to have_selector(search_selector)
end
end
def validate_tomselect_with_search!(page, field_name, search_selector)
open_tomselect_to_validate!(page, field_name) do
expect(page).to have_selector(search_selector)
end
end
def random_producer(product)
Enterprise.is_primary_producer
.where.not(id: product.supplier.id)
.pluck(:name).sample
end
def random_category(variant)
Spree::Taxon
.where.not(id: variant.primary_taxon.id)
.pluck(:name).sample
end
def random_tax_category
Spree::TaxCategory
.pluck(:name).sample
end
end