Add "None" option to tags filter and update search functionality

- Implemented apply_tags_filter method to handle "None" option in tag searches.
- Updated tags select field to include "None" option in the filters.
- Enhanced search_by_tag method in specs  to accept multiple tags and raise an error if none are provided.
- Added tests for searching by "None" tag and combinations with other tags.
This commit is contained in:
Ahmed Ejaz
2026-01-06 02:39:56 +05:00
parent ca14d557c1
commit b8de75b1ef
6 changed files with 94 additions and 12 deletions

View File

@@ -179,6 +179,8 @@ module Admin
product_query = OpenFoodNetwork::Permissions.new(spree_current_user)
.editable_products.merge(product_scope_with_includes).ransack(ransack_query).result
product_query = apply_tags_filter(product_query)
# Postgres requires ORDER BY expressions to appear in the SELECT list when using DISTINCT.
# When the current ransack sort uses the computed stock columns, include them in the select
# so the generated COUNT/DISTINCT query is valid.
@@ -225,12 +227,54 @@ module Admin
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
query.merge!(variants_primary_taxon_id_in: @category_id) if @category_id.present?
query.merge!(variants_tags_name_in: @tags) if @tags.present?
query.merge!(@q) if @q
query
end
# Apply tags filter with OR logic:
# - Products with variants having selected tags
# - OR products with variants having no tags (when "None" is selected)
#
# Note: This cannot be implemented using Ransack because Ransack applies
# AND semantics across associations and cannot express OR logic that combines
# the presence and absence of the same associated records.
def apply_tags_filter(base_query)
return base_query if @tags.blank?
tags = Array(@tags)
none_key = I18n.t('admin.products_v3.filters.tags.none')
has_none = tags.include?(none_key)
tag_names = tags.reject { |t| t == none_key }
queries = []
if tag_names.any?
# Products with at least one variant having one of the selected tags
tagged_product_ids = Spree::Variant
.joins(taggings: :tag)
.where(tags: { name: tag_names })
.select(:product_id)
queries << base_query.where(id: tagged_product_ids)
end
if has_none
# Products where no variants have any tags
tagged_product_ids = Spree::Variant
.joins(:taggings)
.select(:product_id)
queries << base_query.where.not(id: tagged_product_ids)
end
return base_query if queries.empty?
# Combine queries using ActiveRecord's or method
queries.reduce { |combined, query| combined.or(query) }
end
# Optimise by pre-loading required columns
def product_query_includes
[

View File

@@ -23,7 +23,7 @@
- select_tag_options = { class: "fullwidth",
multiple: true ,
data: { controller: "tom-select", "tom-select-placeholder-value": t(".select_tag"), "tom-select-options-value": '{ "maxItems": 5 , "plugins": { "remove_button": {} , "no_active_items": {}, "checkbox_options": { "checkedClassNames": ["ts-checked"], "uncheckedClassNames": ["ts-unchecked"] } } }' } }
= select_tag :tags_name_in, options_for_select(available_tags, tags), select_tag_options
= select_tag :tags_name_in, options_for_select(available_tags.unshift(t('.tags.none')), tags), select_tag_options
.submit
.search-button
= button_tag t(".search"), class: "secondary icon-search relaxed", name: nil

View File

@@ -977,6 +977,7 @@ en:
label: Categories
tags:
label: Tags
none: None
search: Search
sort:
pagination:

View File

@@ -34,8 +34,12 @@ module ProductsHelper
click_button "Search"
end
def search_by_tag(tag)
tomselect_multiselect tag, from: "tags_name_in"
def search_by_tag(*tags)
if tags.empty?
raise ArgumentError, "Please provide at least one tag to search for"
end
tags.each { |tag| tomselect_multiselect tag, from: "tags_name_in" }
click_button "Search"
end

View File

@@ -11,7 +11,7 @@ module TomselectHelper
tomselect_wrapper.find(:css, '.ts-dropdown.multi .ts-dropdown-content .option',
text: value).click
# Close the dropdown
tomselect_wrapper.find(".ts-control").click
page.find("body").click
end
def tomselect_search_and_select(value, options)

View File

@@ -417,16 +417,49 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
context "with variant tag", feature: :variant_tag do
before do
create(:variant, tag_list: "organic")
create_products 1
create(:variant) # without tags
create(:variant)
end
it "can search by tag" do
visit admin_products_url
search_by_tag "organic"
shared_examples "tag search" do
it description do
visit admin_products_url
search_by_tag(*search_tags)
expect(page).to have_select "tags_name_in", selected: "organic"
expect(page).to have_content "1 product found for your search criteria. Showing 1 to 1."
expect_products_count_to_be 1
expect(page).to have_select("tags_name_in", selected: selected_tags)
expect(page).to have_content(result_text)
expect_products_count_to_be(expected_count)
end
end
context "when searching by a single tag" do
let(:description) { "returns variants with that tag" }
let(:search_tags) { ["organic"] }
let(:selected_tags) { "organic" }
let(:expected_count) { 1 }
let(:result_text) { "1 product found for your search criteria. Showing 1 to 1." }
include_examples "tag search"
end
context "when searching by None tag" do
let(:description) { "returns variants without tags" }
let(:search_tags) { ["None"] }
let(:selected_tags) { "None" }
let(:expected_count) { 2 }
let(:result_text) { "2 products found for your search criteria. Showing 1 to 2." }
include_examples "tag search"
end
context "when searching by None and another tag" do
let(:description) { "returns variants with either no tags or the given tag" }
let(:search_tags) { ["None", "organic"] }
let(:selected_tags) { ["None", "organic"] }
let(:expected_count) { 3 }
let(:result_text) { "3 products found for your search criteria. Showing 1 to 3." }
include_examples "tag search"
end
end
end