diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index ef974b299f3..8c90310d8d6 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -133,13 +133,26 @@ export function applyDefaultHandlers(pretender) { pretender.delete("/bookmarks/:id", () => response({})); - pretender.get("/tags/filter/search", () => { - return response({ + pretender.get("/tags/filter/search", (request) => { + const responseBody = { results: [ { id: "monkey", name: "monkey", count: 1 }, { id: "gazelle", name: "gazelle", count: 2 }, ], - }); + }; + + if ( + request.queryParams.categoryId === "1" && + request.queryParams.q === "" && + !request.queryParams.selected_tags.includes("monkey") + ) { + responseBody["required_tag_group"] = { + name: "monkey group", + min_count: 1, + }; + } + + return response(responseBody); }); pretender.get(`/u/:username/emails.json`, (request) => { diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js index e1bb328e833..d239a2c6472 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/mini-tag-chooser-test.js @@ -3,6 +3,7 @@ import componentTest, { } from "discourse/tests/helpers/component-test"; import { discourseModule, + query, queryAll, } from "discourse/tests/helpers/qunit-helpers"; import I18n from "I18n"; @@ -81,5 +82,34 @@ discourseModule( ); }, }); + + componentTest("required_tag_group", { + template: hbs`{{mini-tag-chooser value=value options=(hash categoryId=1)}}`, + + beforeEach() { + this.set("value", ["foo", "bar"]); + }, + + async test(assert) { + assert.strictEqual(this.subject.header().value(), "foo,bar"); + + await this.subject.expand(); + + assert.strictEqual( + query("input[name=filter-input-search]").placeholder, + I18n.t("tagging.choose_for_topic_required_group", { + count: 1, + name: "monkey group", + }) + ); + + await this.subject.selectRowByValue("monkey"); + + assert.strictEqual( + query("input[name=filter-input-search]").placeholder, + I18n.t("select_kit.filter_placeholder") + ); + }, + }); } ); diff --git a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js index 59d26fe729d..7a8aa6d0a67 100644 --- a/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/mini-tag-chooser.js @@ -96,6 +96,18 @@ export default MultiSelectComponent.extend(TagsMixin, { results = results.sort((a, b) => a.text.localeCompare(b.text)); } + if (json.required_tag_group) { + context.set( + "selectKit.options.translatedFilterPlaceholder", + I18n.t("tagging.choose_for_topic_required_group", { + count: json.required_tag_group.min_count, + name: json.required_tag_group.name, + }) + ); + } else { + context.set("selectKit.options.translatedFilterPlaceholder", null); + } + return results.filter((r) => !makeArray(context.tags).includes(r.id)); }, }); diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 84712001e53..7d3012bb652 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -246,9 +246,10 @@ class TagsController < ::ApplicationController filter_params[:order_popularity] = true end - tags_with_counts = DiscourseTagging.filter_allowed_tags( + tags_with_counts, filter_result_context = DiscourseTagging.filter_allowed_tags( guardian, - filter_params + **filter_params, + with_context: true ) tags = self.class.tag_counts_json(tags_with_counts, show_pm_tags: guardian.can_tag_pms?) @@ -281,6 +282,10 @@ class TagsController < ::ApplicationController end end + if required_tag_group = filter_result_context[:required_tag_group] + json_response[:required_tag_group] = required_tag_group + end + render json: json_response end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8e2a03ab53f..f7f578556eb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3836,6 +3836,9 @@ en: choose_for_topic_required: one: "select at least %{count} tag..." other: "select at least %{count} tags..." + choose_for_topic_required_group: + one: "select %{count} tag from '%{name}'..." + other: "select %{count} tags from '%{name}'..." info: "Info" default_info: "This tag isn't restricted to any categories, and has no synonyms." staff_info: "To add restrictions, put this tag in a tag group." diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index 77487c23159..8e64bab2a56 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -367,11 +367,13 @@ module DiscourseTagging # - there are more available tags than the query limit # - and no search term has been included required_tag_ids = nil + required_category_tag_group = nil if opts[:for_input] && category&.category_required_tag_groups.present? && (filter_for_non_staff || term.blank?) category.category_required_tag_groups.each do |crtg| group_tags = crtg.tag_group.tags.pluck(:id) next if (group_tags & selected_tag_ids).size >= crtg.min_count if filter_for_non_staff || group_tags.size >= opts[:limit].to_i + required_category_tag_group = crtg required_tag_ids = group_tags builder.where("id IN (?)", required_tag_ids) end @@ -441,6 +443,19 @@ module DiscourseTagging end result = builder.query(builder_params).uniq { |t| t.id } + + if opts[:with_context] + context = {} + if required_category_tag_group + context[:required_tag_group] = { + name: required_category_tag_group.tag_group.name, + min_count: required_category_tag_group.min_count + } + end + [result, context] + else + result + end end def self.filter_visible(query, guardian = nil) diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index 58b28f3870a..0ee9efe7825 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -877,6 +877,28 @@ describe TagsController do expect(response.status).to eq(400) expect(response.parsed_body['errors'].first).to eq(I18n.t('invalid_params', message: 'limit')) end + + it 'includes required tag group information' do + tag1 = Fabricate(:tag) + tag2 = Fabricate(:tag) + + tag_group = Fabricate(:tag_group, tags: [tag1, tag2]) + crtg = CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1) + category = Fabricate(:category, category_required_tag_groups: [ crtg ]) + + get "/tags/filter/search.json", params: { q: '', categoryId: category.id, filterForInput: true } + expect(response.status).to eq(200) + expect(response.parsed_body["results"].map { |t| t["name"] }).to contain_exactly(tag1.name, tag2.name) + expect(response.parsed_body["required_tag_group"]).to eq({ + "name" => tag_group.name, + "min_count" => crtg.min_count + }) + + get "/tags/filter/search.json", params: { q: '', categoryId: category.id, filterForInput: true, selected_tags: [tag1.name] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"].map { |t| t["name"] }).to contain_exactly(tag2.name) + expect(response.parsed_body["required_tag_group"]).to eq(nil) + end end end