diff --git a/app/assets/javascripts/discourse/app/components/search-advanced-options.js b/app/assets/javascripts/discourse/app/components/search-advanced-options.js index 86d11e1bf09..5b72e095f46 100644 --- a/app/assets/javascripts/discourse/app/components/search-advanced-options.js +++ b/app/assets/javascripts/discourse/app/components/search-advanced-options.js @@ -263,34 +263,33 @@ export default Component.extend({ const subcategories = match[0] .replace(REGEXP_CATEGORY_PREFIX, "") .split(":"); + + let userInput; if (subcategories.length > 1) { - const userInput = Category.findBySlug( - subcategories[1], - subcategories[0] + userInput = Category.list().find( + (category) => + category.get("parentCategory.slug") === subcategories[0] && + category.slug === subcategories[1] ); - if ( - (!existingInput && userInput) || - (existingInput && userInput && existingInput.id !== userInput.id) - ) { - this.set("searchedTerms.category", userInput); - } - } else if (isNaN(subcategories)) { - const userInput = Category.findSingleBySlug(subcategories[0]); - if ( - (!existingInput && userInput) || - (existingInput && userInput && existingInput.id !== userInput.id) - ) { - this.set("searchedTerms.category", userInput); - } } else { - const userInput = Category.findById(subcategories[0]); - if ( - (!existingInput && userInput) || - (existingInput && userInput && existingInput.id !== userInput.id) - ) { - this.set("searchedTerms.category", userInput); + userInput = Category.list().find( + (category) => + !category.parentCategory && category.slug === subcategories[0] + ); + + if (!userInput) { + userInput = Category.list().find( + (category) => category.slug === subcategories[0] + ); } } + + if ( + (!existingInput && userInput) || + (existingInput && userInput && existingInput.id !== userInput.id) + ) { + this.set("searchedTerms.category", userInput); + } } else { this.set("searchedTerms.category", null); } diff --git a/app/assets/javascripts/discourse/app/widgets/category-link.js b/app/assets/javascripts/discourse/app/widgets/category-link.js index 1963962908f..9795fde0296 100644 --- a/app/assets/javascripts/discourse/app/widgets/category-link.js +++ b/app/assets/javascripts/discourse/app/widgets/category-link.js @@ -4,7 +4,7 @@ import { categoryBadgeHTML } from "discourse/helpers/category-link"; // Right now it's RawHTML. Eventually it should emit nodes export default class CategoryLink extends RawHtml { constructor(attrs) { - attrs.html = categoryBadgeHTML(attrs.category, attrs); + attrs.html = `${categoryBadgeHTML(attrs.category, attrs)}`; super(attrs); } } diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js index 32457733eb5..6da7139e51e 100644 --- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js +++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js @@ -374,9 +374,11 @@ createWidget("search-menu-assistant", { switch (suggestionKeyword) { case "#": attrs.results.forEach((category) => { - const slug = prefix - ? `${prefix} #${category.slug} ` - : `#${category.slug} `; + const fullSlug = category.parentCategory + ? `#${category.parentCategory.slug}:${category.slug}` + : `#${category.slug}`; + + const slug = prefix ? `${prefix} ${fullSlug} ` : `${fullSlug} `; content.push( this.attach("search-menu-assistant-item", { @@ -433,6 +435,7 @@ createWidget("search-menu-assistant-item", { this.attach("category-link", { category: attrs.category, allowUncategorized: true, + recursive: true, }), ] ); diff --git a/lib/search.rb b/lib/search.rb index 0bf753df8fe..a2e024c4dd2 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -511,12 +511,7 @@ class Search category_ids = Category.where('slug ilike ? OR name ilike ? OR id = ?', match, match, match.to_i).pluck(:id) if category_ids.present? - - unless exact - category_ids += - Category.where('parent_category_id = ?', category_ids.first).pluck(:id) - end - + category_ids += Category.subcategory_ids(category_ids.first) unless exact @category_filter_matched ||= true posts.where("topics.category_id IN (?)", category_ids) else @@ -525,44 +520,31 @@ class Search end advanced_filter(/^\#([\p{L}\p{M}0-9\-:=]+)$/i) do |posts, match| - - exact = true - category_slug, subcategory_slug = match.to_s.split(":") next unless category_slug - if subcategory_slug - - category_id, _ = DB.query_single(<<~SQL, category_slug.downcase, subcategory_slug.downcase) - SELECT sub.id - FROM categories sub - JOIN categories c ON sub.parent_category_id = c.id - WHERE LOWER(c.slug) = ? AND LOWER(sub.slug) = ? - ORDER BY c.id - LIMIT 1 - SQL - + exact = true + if category_slug[0] == "=" + category_slug = category_slug[1..-1] else - # main category - if category_slug[0] == "=" - category_slug = category_slug[1..-1] - else - exact = false - end + exact = false + end - category_id = Category.where("lower(slug) = ?", category_slug.downcase) + category_id = if subcategory_slug + Category + .where('lower(slug) = ?', subcategory_slug.downcase) + .where(parent_category_id: Category.where('lower(slug) = ?', category_slug.downcase).select(:id)) + .pluck_first(:id) + else + Category + .where('lower(slug) = ?', category_slug.downcase) .order('case when parent_category_id is null then 0 else 1 end') - .pluck(:id) - .first + .pluck_first(:id) end if category_id category_ids = [category_id] - - unless exact - category_ids += - Category.where('parent_category_id = ?', category_id).pluck(:id) - end + category_ids += Category.subcategory_ids(category_id) if !exact @category_filter_matched ||= true posts.where("topics.category_id IN (?)", category_ids) diff --git a/spec/lib/search_spec.rb b/spec/lib/search_spec.rb index 6ed833283e3..63f28b7ecb7 100644 --- a/spec/lib/search_spec.rb +++ b/spec/lib/search_spec.rb @@ -166,4 +166,32 @@ describe Search do ]) end end + + context "categories" do + it "finds topics in sub-sub-categories" do + SiteSetting.max_category_nesting = 3 + + category = Fabricate(:category_with_definition) + subcategory = Fabricate(:category_with_definition, parent_category_id: category.id) + subsubcategory = Fabricate(:category_with_definition, parent_category_id: subcategory.id) + + topic = Fabricate(:topic, category: subsubcategory) + post = Fabricate(:post, topic: topic) + + SearchIndexer.enable + SearchIndexer.index(post, force: true) + + expect(Search.execute("test ##{category.slug}").posts).to contain_exactly(post) + expect(Search.execute("test ##{category.slug}:#{subcategory.slug}").posts).to contain_exactly(post) + expect(Search.execute("test ##{subcategory.slug}").posts).to contain_exactly(post) + expect(Search.execute("test ##{subcategory.slug}:#{subsubcategory.slug}").posts).to contain_exactly(post) + expect(Search.execute("test ##{subsubcategory.slug}").posts).to contain_exactly(post) + + expect(Search.execute("test #=#{category.slug}").posts).to be_empty + expect(Search.execute("test #=#{category.slug}:#{subcategory.slug}").posts).to be_empty + expect(Search.execute("test #=#{subcategory.slug}").posts).to be_empty + expect(Search.execute("test #=#{subcategory.slug}:#{subsubcategory.slug}").posts).to contain_exactly(post) + expect(Search.execute("test #=#{subsubcategory.slug}").posts).to contain_exactly(post) + end + end end