diff --git a/app/controllers/admin/products_v3_controller.rb b/app/controllers/admin/products_v3_controller.rb index 80b99aed73..ff3bc8d433 100644 --- a/app/controllers/admin/products_v3_controller.rb +++ b/app/controllers/admin/products_v3_controller.rb @@ -4,6 +4,7 @@ module Admin class ProductsV3Controller < Spree::Admin::BaseController helper ProductsHelper + include ::Products::AjaxSearch before_action :init_filters_params before_action :init_pagination_params diff --git a/app/controllers/concerns/products/ajax_search.rb b/app/controllers/concerns/products/ajax_search.rb new file mode 100644 index 0000000000..37cec78e0a --- /dev/null +++ b/app/controllers/concerns/products/ajax_search.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Products + module AjaxSearch + extend ActiveSupport::Concern + + def search_producers + query = OpenFoodNetwork::Permissions.new(spree_current_user) + .managed_product_enterprises.is_primary_producer.by_name + + render json: build_search_response(query) + end + + def search_categories + query = Spree::Taxon.all + + render json: build_search_response(query) + end + + def search_tax_categories + query = Spree::TaxCategory.all + + render json: build_search_response(query) + end + + private + + def build_search_response(query) + page = (params[:page] || 1).to_i + per_page = 30 + + filtered_query = apply_search_filter(query) + total_count = filtered_query.size + items = paginated_items(filtered_query, page, per_page) + results = format_results(items) + + { results: results, pagination: { more: (page * per_page) < total_count } } + end + + def apply_search_filter(query) + search_term = params[:q] + return query if search_term.blank? + + escaped_search_term = ActiveRecord::Base.sanitize_sql_like(search_term) + pattern = "%#{escaped_search_term}%" + + query.where('name ILIKE ?', pattern) + end + + def paginated_items(query, page, per_page) + query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id) + end + + def format_results(items) + items.map { |label, value| { value:, label: } } + end + end +end diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb index 8b07a650d9..1b10ea9330 100644 --- a/app/helpers/admin/products_helper.rb +++ b/app/helpers/admin/products_helper.rb @@ -52,5 +52,15 @@ module Admin @allowed_source_producers ||= OpenFoodNetwork::Permissions.new(spree_current_user) .enterprises_granting_linked_variants end + + # Query only name of the model to avoid loading the whole record + def selected_option(id, model) + return [] unless id + + name = model.where(id: id).pick(:name) + return [] unless name + + [[name, id]] + end end end diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index c8e4e19831..0de27a0718 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -221,9 +221,18 @@ module Spree OpenFoodNetwork::Permissions.new(user). enterprises_granting_linked_variants.include? variant.supplier end - - can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone, - :create_linked_variant], :products_v3 + can [ + :admin, + :index, + :bulk_update, + :destroy, + :destroy_variant, + :clone, + :create_linked_variant, + :search_producers, + :search_categories, + :search_tax_categories + ], :products_v3 can [:create], Spree::Variant can [:admin, :index, :read, :edit, diff --git a/app/views/admin/products_v3/_filters.html.haml b/app/views/admin/products_v3/_filters.html.haml index 68e0c88ebb..93e60182f8 100644 --- a/app/views/admin/products_v3/_filters.html.haml +++ b/app/views/admin/products_v3/_filters.html.haml @@ -9,14 +9,22 @@ - if producer_options.many? .producers = label_tag :producer_id, t('.producers.label') - = select_tag :producer_id, options_for_select(producer_options, producer_id), - include_blank: t('.all_producers'), class: "fullwidth", - data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_producers')} + = render(SearchableDropdownComponent.new(name: :producer_id, + aria_label: t('.producer_field_name'), + options: selected_option(producer_id, Enterprise), + selected_option: producer_id, + remote_url: admin_products_search_producers_url, + include_blank: t('.all_producers'), + placeholder_value: t('.search_for_producers'))) .categories = label_tag :category_id, t('.categories.label') - = select_tag :category_id, options_for_select(category_options, category_id), - include_blank: t('.all_categories'), class: "fullwidth", - data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_categories')} + = render(SearchableDropdownComponent.new(name: :category_id, + aria_label: t('.category_field_name'), + options: selected_option(category_id, Spree::Taxon), + selected_option: category_id, + remote_url: admin_products_search_categories_url, + include_blank: t('.all_categories'), + placeholder_value: t('.search_for_categories'))) -if variant_tag_enabled?(spree_current_user) .tags = label_tag :tags_name_in, t('.tags.label') diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index cd9e8ae6ee..534c39b8e3 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -59,27 +59,27 @@ = render(SearchableDropdownComponent.new(form: f, name: :supplier_id, aria_label: t('.producer_field_name'), - options: producer_options, + options: variant.supplier_id ? [[variant.supplier.name, variant.supplier_id]] : [], selected_option: variant.supplier_id, - include_blank: t('admin.products_v3.filters.select_producer'), + remote_url: admin_products_search_producers_url, placeholder_value: t('admin.products_v3.filters.select_producer'))) = error_message_on variant, :supplier %td.col-category.field.naked_inputs = render(SearchableDropdownComponent.new(form: f, name: :primary_taxon_id, - options: category_options, + options: variant.primary_taxon_id ? [[variant.primary_taxon.name, variant.primary_taxon_id]] : [], selected_option: variant.primary_taxon_id, aria_label: t('.category_field_name'), - include_blank: t('admin.products_v3.filters.select_category'), + remote_url: admin_products_search_categories_url, placeholder_value: t('admin.products_v3.filters.select_category'))) = error_message_on variant, :primary_taxon %td.col-tax_category.field.naked_inputs = render(SearchableDropdownComponent.new(form: f, name: :tax_category_id, - options: tax_category_options, + options: variant.tax_category_id ? [[variant.tax_category.name, variant.tax_category_id]] : [], selected_option: variant.tax_category_id, - include_blank: t('.none_tax_category'), aria_label: t('.tax_category_field_name'), + remote_url: admin_products_search_tax_categories_url, placeholder_value: t('.search_for_tax_categories'))) = error_message_on variant, :tax_category - if variant_tag_enabled?(spree_current_user) diff --git a/app/webpacker/controllers/tom_select_controller.js b/app/webpacker/controllers/tom_select_controller.js index 3867fa3380..203f3ce2b2 100644 --- a/app/webpacker/controllers/tom_select_controller.js +++ b/app/webpacker/controllers/tom_select_controller.js @@ -83,6 +83,9 @@ export default class extends Controller { } #addRemoteOptions(options) { + // by default, for dropdown_input plugin, it's true. Otherwise for multi-select it's false + // it should always be true so to invoke the onDropdownOpen to fetch options + options.shouldOpen = true; this.openedByClick = false; options.firstUrl = (query) => { @@ -91,12 +94,9 @@ export default class extends Controller { options.load = this.#fetchOptions.bind(this); - options.onFocus = function () { - this.control.load("", () => {}); - }.bind(this); - options.onDropdownOpen = function () { this.openedByClick = true; + this.control.load("", () => {}); }.bind(this); options.onType = function () { diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 33553f5a51..bfc9434a41 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -83,6 +83,9 @@ Openfoodnetwork::Application.routes.draw do delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' post 'products/create_linked_variant', to: 'products_v3#create_linked_variant', as: 'create_linked_variant' + get 'products_v3/search_producers', to: 'products_v3#search_producers', as: 'products_search_producers' + get 'products_v3/search_categories', to: 'products_v3#search_categories', as: 'products_search_categories' + get 'products_v3/search_tax_categories', to: 'products_v3#search_tax_categories', as: 'products_search_tax_categories' resources :product_preview, only: [:show] resources :variant_overrides do diff --git a/spec/javascripts/stimulus/tom_select_controller_test.js b/spec/javascripts/stimulus/tom_select_controller_test.js index 4f4705e222..c1293e2eb2 100644 --- a/spec/javascripts/stimulus/tom_select_controller_test.js +++ b/spec/javascripts/stimulus/tom_select_controller_test.js @@ -166,7 +166,6 @@ describe("TomSelectController", () => { expect(settings.searchField).toBe("label"); expect(settings.load).toEqual(expect.any(Function)); expect(settings.firstUrl).toEqual(expect.any(Function)); - expect(settings.onFocus).toEqual(expect.any(Function)); }); it("fetches page 1 on focus", async () => {