mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
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:
43
app/components/searchable_dropdown_component.rb
Normal file
43
app/components/searchable_dropdown_component.rb
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
43
spec/support/request/tomselect_helper.rb
Normal file
43
spec/support/request/tomselect_helper.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user