FEATURE: Display error message when category restriction is applied for tags

FEATURE: Display error message when category restriction is applied for tags

diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
index 0b2f9a7..f263fc1 100644
--- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
+++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6
@@ -182,6 +182,7 @@ export default ComboBox.extend(TagsMixin, {
     let results = json.results;
 
     context.set("termMatchesForbidden", json.forbidden ? true : false);
+    context.set("termMatchErrorMessage", json.forbidden_message);
 
     if (context.get("siteSettings.tags_sort_alphabetically")) {
       results = results.sort((a, b) => a.id > b.id);
diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6
index b907e87..af33293 100644
--- a/app/assets/javascripts/select-kit/components/select-kit.js.es6
+++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6
@@ -277,7 +277,9 @@ export default Ember.Component.extend(
         collectionComputedContent.length === 0 &&
         !isLoading
       ) {
-        return I18n.t("select_kit.no_content");
+        return (
+          this.get("termMatchErrorMessage") || I18n.t("select_kit.no_content")
+        );
       }
     },
 
diff --git a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6
index 90c291b..807b86b 100644
--- a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6
+++ b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6
@@ -27,6 +27,7 @@ export default MultiSelectComponent.extend(TagsMixin, {
     }
 
     this.set("termMatchesForbidden", false);
+    this.set("termMatchErrorMessage", null);
 
     this.set("templateForRow", rowComponent => {
       const tag = rowComponent.get("computedContent");
@@ -117,6 +118,7 @@ export default MultiSelectComponent.extend(TagsMixin, {
     let results = json.results;
 
     context.set("termMatchesForbidden", json.forbidden ? true : false);
+    context.set("termMatchErrorMessage", json.forbidden_message);
 
     if (context.get("blacklist")) {
       results = results.filter(result => {
diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss
index cd5c757..a96520b 100644
--- a/app/assets/stylesheets/common/select-kit/select-kit.scss
+++ b/app/assets/stylesheets/common/select-kit/select-kit.scss
@@ -125,10 +125,6 @@
     align-items: center;
     justify-content: flex-start;
 
-    &.no-content {
-      white-space: nowrap;
-    }
-
     .name {
       margin: 0;
       overflow: hidden;
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 2c02fc7..f1e49da 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -210,9 +210,24 @@ class TagsController < ::ApplicationController
 
     json_response = { results: tags }
 
-    if Tag.where_name(clean_name).exists? && !tags.find { |h| h[:id].downcase == clean_name.downcase }
+    if !tags.find { |h| h[:id].downcase == clean_name.downcase } && tag = Tag.where_name(clean_name).first
       # filter_allowed_tags determined that the tag entered is not allowed
       json_response[:forbidden] = params[:q]
+
+      category_names = tag.categories.where(id: guardian.allowed_category_ids).pluck(:name)
+      category_names += Category.joins(tag_groups: :tags).where(id: guardian.allowed_category_ids, "tags.id": tag.id).pluck(:name)
+
+      if category_names.present?
+        category_names.uniq!
+        json_response[:forbidden_message] = I18n.t(
+          "tags.forbidden.restricted_to",
+          count: category_names.count,
+          tag_name: tag.name,
+          category_names: category_names.join(", ")
+        )
+      else
+        json_response[:forbidden_message] = I18n.t("tags.forbidden.in_this_category", tag_name: tag.name)
+      end
     end
 
     render json: json_response
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index c8e1621..f585ac9 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -4052,6 +4052,11 @@ en:
     staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
     minimum_required_tags: "You must select at least %{count} tags."
     upload_row_too_long: "The CSV file should have one tag per line. Optionally the tag can be followed by a comma, then the tag group name."
+    forbidden:
+      in_this_category: "\"%{tag_name}\" cannot be used in this category"
+      restricted_to:
+        one: "\"%{tag_name}\" is restricted to the \"%{category_names}\" category"
+        other: "\"%{tag_name}\" is restricted to the following categories: %{category_names}"
   rss_by_tag: "Topics tagged %{tag}"
 
   finish_installation:
diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb
index 740e628..ade557a 100644
--- a/spec/requests/tags_controller_spec.rb
+++ b/spec/requests/tags_controller_spec.rb
@@ -319,14 +319,33 @@ describe TagsController do
         expect(json['results'].map { |j| j['id'] }).to eq(['tag', 'tag2'])
       end
 
-      it "can say if given tag is not allowed" do
-        yup, nope = Fabricate(:tag, name: 'yup'), Fabricate(:tag, name: 'nope')
-        category = Fabricate(:category, tags: [yup])
-        get "/tags/filter/search.json", params: { q: 'nope', categoryId: category.id }
-        expect(response.status).to eq(200)
-        json = ::JSON.parse(response.body)
-        expect(json["results"].map { |j| j["id"] }.sort).to eq([])
-        expect(json["forbidden"]).to be_present
+      context 'with category restriction' do
+        let(:yup) { Fabricate(:tag, name: 'yup') }
+        let(:category) { Fabricate(:category, tags: [yup]) }
+
+        it "can say if given tag is not allowed" do
+          nope = Fabricate(:tag, name: 'nope')
+          get "/tags/filter/search.json", params: { q: nope.name, categoryId: category.id }
+          expect(response.status).to eq(200)
+          json = ::JSON.parse(response.body)
+          expect(json["results"].map { |j| j["id"] }.sort).to eq([])
+          expect(json["forbidden"]).to be_present
+          expect(json["forbidden_message"]).to eq(I18n.t("tags.forbidden.in_this_category", tag_name: nope.name))
+        end
+
+        it "can say if given tag is restricted to different category" do
+          category
+          get "/tags/filter/search.json", params: { q: yup.name, categoryId: Fabricate(:category).id }
+          json = ::JSON.parse(response.body)
+          expect(json["results"].map { |j| j["id"] }.sort).to eq([])
+          expect(json["forbidden"]).to be_present
+          expect(json["forbidden_message"]).to eq(I18n.t(
+            "tags.forbidden.restricted_to",
+            count: 1,
+            tag_name: yup.name,
+            category_names: category.name
+          ))
+        end
       end
 
       it "matches tags after sanitizing input" do

GitHub
sha: 385829d7

This commit has been mentioned on Discourse Meta. There might be relevant details there: