diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js
index e68af1d6031..b4f79de8b78 100644
--- a/app/assets/javascripts/discourse/app/models/category.js
+++ b/app/assets/javascripts/discourse/app/models/category.js
@@ -400,6 +400,7 @@ export default class Category extends RestModel {
categories: result["categories"].map((category) =>
Site.current().updateCategory(category)
),
+ categoriesCount: result["categories_count"],
};
} else {
return result["categories"].map((category) =>
diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js
index 116e2dfb862..2c65d0008e2 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-drop-test.js
@@ -68,7 +68,7 @@ module("Integration | Component | select-kit/category-drop", function (hooks) {
const text = this.subject.header().label();
assert.strictEqual(
text,
- I18n.t("category.all").toLowerCase(),
+ I18n.t("categories.categories_label"),
"it uses the noneLabel"
);
});
diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop-more-collection.gjs b/app/assets/javascripts/select-kit/addon/components/category-drop-more-collection.gjs
new file mode 100644
index 00000000000..ccabf247a35
--- /dev/null
+++ b/app/assets/javascripts/select-kit/addon/components/category-drop-more-collection.gjs
@@ -0,0 +1,44 @@
+import Component from "@glimmer/component";
+import { hash } from "@ember/helper";
+import { LinkTo } from "@ember/routing";
+import { inject as service } from "@ember/service";
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+import {
+ ALL_CATEGORIES_ID,
+ NO_CATEGORIES_ID,
+} from "select-kit/components/category-drop";
+
+export default class CategoryDropMoreCollection extends Component {
+ @service site;
+
+ tagName = "";
+
+ get moreCount() {
+ if (!this.args.selectKit.totalCount) {
+ return 0;
+ }
+
+ const currentCount = this.args.collection.content.filter(
+ (category) =>
+ category.id !== NO_CATEGORIES_ID && category.id !== ALL_CATEGORIES_ID
+ ).length;
+
+ return this.args.selectKit.totalCount - currentCount;
+ }
+
+
+ {{#if this.moreCount}}
+
+ {{/if}}
+
+}
diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop.js b/app/assets/javascripts/select-kit/addon/components/category-drop.js
index 684fa8380d8..282892e9e52 100644
--- a/app/assets/javascripts/select-kit/addon/components/category-drop.js
+++ b/app/assets/javascripts/select-kit/addon/components/category-drop.js
@@ -9,18 +9,22 @@ import DiscourseURL, {
} from "discourse/lib/url";
import Category from "discourse/models/category";
import I18n from "discourse-i18n";
+import CategoryDropMoreCollection from "select-kit/components/category-drop-more-collection";
import CategoryRow from "select-kit/components/category-row";
import ComboBoxComponent from "select-kit/components/combo-box";
+import { MAIN_COLLECTION } from "select-kit/components/select-kit";
export const NO_CATEGORIES_ID = "no-categories";
export const ALL_CATEGORIES_ID = "all-categories";
+const MORE_COLLECTION = "MORE_COLLECTION";
+
export default ComboBoxComponent.extend({
pluginApiIdentifiers: ["category-drop"],
classNames: ["category-drop"],
value: readOnly("category.id"),
content: readOnly("categoriesWithShortcuts.[]"),
- noCategoriesLabel: I18n.t("categories.no_subcategory"),
+ noCategoriesLabel: I18n.t("categories.no_subcategories"),
navigateToEdit: false,
editingCategory: false,
editingCategoryTab: null,
@@ -44,6 +48,18 @@ export default ComboBoxComponent.extend({
allowUncategorized: "allowUncategorized",
},
+ init() {
+ this._super(...arguments);
+
+ this.insertAfterCollection(MAIN_COLLECTION, MORE_COLLECTION);
+ },
+
+ modifyComponentForCollection(collection) {
+ if (collection === MORE_COLLECTION) {
+ return CategoryDropMoreCollection;
+ }
+ },
+
modifyComponentForRow() {
return CategoryRow;
},
@@ -85,6 +101,12 @@ export default ComboBoxComponent.extend({
});
}
+ // If there is a single shortcut, we can have a single "remove filter"
+ // option
+ if (shortcuts.length === 1 && shortcuts[0].id === ALL_CATEGORIES_ID) {
+ shortcuts[0].name = I18n.t("categories.remove_filter");
+ }
+
return shortcuts;
}
),
@@ -96,9 +118,17 @@ export default ComboBoxComponent.extend({
modifyNoSelection() {
if (this.selectKit.options.noSubcategories) {
- return this.defaultItem(NO_CATEGORIES_ID, this.noCategoriesLabel);
+ return this.defaultItem(
+ NO_CATEGORIES_ID,
+ I18n.t("categories.no_subcategories")
+ );
} else {
- return this.defaultItem(ALL_CATEGORIES_ID, this.allCategoriesLabel);
+ return this.defaultItem(
+ ALL_CATEGORIES_ID,
+ this.selectKit.options.subCategory
+ ? I18n.t("categories.subcategories_label")
+ : I18n.t("categories.categories_label")
+ );
}
},
@@ -149,16 +179,23 @@ export default ComboBoxComponent.extend({
parentCategoryId = -1;
}
- const results = (
- await Category.asyncSearch(filter, {
- parentCategoryId,
- includeUncategorized: this.siteSettings.allow_uncategorized_topics,
- includeAncestors: true,
- limit: 15,
- })
- ).categories;
+ const result = await Category.asyncSearch(filter, {
+ parentCategoryId,
+ includeUncategorized: this.siteSettings.allow_uncategorized_topics,
+ includeAncestors: true,
+ // Show all categories if possible (up to 18), otherwise show just
+ // first 15 and let CategoryDropMoreCollection show the "show more" link
+ limit: 18,
+ });
- return this.shortcuts.concat(results);
+ const categories =
+ result.categoriesCount > 18
+ ? result.categories.slice(0, 15)
+ : result.categories;
+
+ this.selectKit.totalCount = result.categoriesCount;
+
+ return this.shortcuts.concat(categories);
}
const opts = {
diff --git a/app/assets/javascripts/select-kit/addon/components/tag-drop.js b/app/assets/javascripts/select-kit/addon/components/tag-drop.js
index 49a6b7a155f..040b4440c58 100644
--- a/app/assets/javascripts/select-kit/addon/components/tag-drop.js
+++ b/app/assets/javascripts/select-kit/addon/components/tag-drop.js
@@ -1,8 +1,9 @@
import { computed } from "@ember/object";
-import { equal, readOnly } from "@ember/object/computed";
-import { i18n, setting } from "discourse/lib/computed";
+import { readOnly } from "@ember/object/computed";
+import { setting } from "discourse/lib/computed";
import DiscourseURL, { getCategoryAndTagUrl } from "discourse/lib/url";
import { makeArray } from "discourse-common/lib/helpers";
+import I18n from "discourse-i18n";
import ComboBoxComponent from "select-kit/components/combo-box";
import FilterForMore from "select-kit/components/filter-for-more";
import { MAIN_COLLECTION } from "select-kit/components/select-kit";
@@ -10,7 +11,8 @@ import TagsMixin from "select-kit/mixins/tags";
export const NO_TAG_ID = "no-tags";
export const ALL_TAGS_ID = "all-tags";
-export const NONE_TAG_ID = "none";
+
+export const NONE_TAG = "none";
const MORE_TAGS_COLLECTION = "MORE_TAGS_COLLECTION";
@@ -45,8 +47,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
autoInsertNoneItem: false,
},
- noTagsSelected: equal("tagId", NONE_TAG_ID),
-
init() {
this._super(...arguments);
@@ -68,20 +68,18 @@ export default ComboBoxComponent.extend(TagsMixin, {
},
modifyNoSelection() {
- if (this.noTagsSelected) {
- return this.defaultItem(NO_TAG_ID, this.noTagsLabel);
+ if (this.tagId === NONE_TAG) {
+ return this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
} else {
- return this.defaultItem(ALL_TAGS_ID, this.allTagsLabel);
+ return this.defaultItem(ALL_TAGS_ID, I18n.t("tagging.selector_tags"));
}
},
modifySelection(content) {
- if (this.tagId) {
- if (this.noTagsSelected) {
- content = this.defaultItem(NO_TAG_ID, this.noTagsLabel);
- } else {
- content = this.defaultItem(this.tagId, this.tagId);
- }
+ if (this.tagId === NONE_TAG) {
+ content = this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
+ } else if (this.tagId) {
+ content = this.defaultItem(this.tagId, this.tagId);
}
return content;
@@ -91,10 +89,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
return this.tagId ? `tag-${this.tagId}` : "tag_all";
}),
- allTagsLabel: i18n("tagging.selector_all_tags"),
-
- noTagsLabel: i18n("tagging.selector_no_tags"),
-
modifyComponentForRow() {
return "tag-row";
},
@@ -102,15 +96,24 @@ export default ComboBoxComponent.extend(TagsMixin, {
shortcuts: computed("tagId", function () {
const shortcuts = [];
- if (this.tagId !== NONE_TAG_ID) {
+ if (this.tagId !== NONE_TAG) {
shortcuts.push({
id: NO_TAG_ID,
- name: this.noTagsLabel,
+ name: I18n.t("tagging.selector_no_tags"),
});
}
if (this.tagId) {
- shortcuts.push({ id: ALL_TAGS_ID, name: this.allTagsLabel });
+ shortcuts.push({
+ id: ALL_TAGS_ID,
+ name: I18n.t("tagging.selector_all_tags"),
+ });
+ }
+
+ // If there is a single shortcut, we can have a single "remove filter"
+ // option
+ if (shortcuts.length === 1 && shortcuts[0].id === ALL_TAGS_ID) {
+ shortcuts[0].name = I18n.t("tagging.selector_remove_filter");
}
return shortcuts;
@@ -173,7 +176,7 @@ export default ComboBoxComponent.extend(TagsMixin, {
actions: {
onChange(tagId, tag) {
if (tagId === NO_TAG_ID) {
- tagId = NONE_TAG_ID;
+ tagId = NONE_TAG;
} else if (tagId === ALL_TAGS_ID) {
tagId = null;
} else if (tag && tag.targetTagId) {
diff --git a/app/assets/javascripts/select-kit/addon/components/tag-row.hbs b/app/assets/javascripts/select-kit/addon/components/tag-row.hbs
index d1705178afe..e4a44b404e7 100644
--- a/app/assets/javascripts/select-kit/addon/components/tag-row.hbs
+++ b/app/assets/javascripts/select-kit/addon/components/tag-row.hbs
@@ -1 +1,5 @@
-{{discourse-tag this.rowValue noHref=true count=this.item.count}}
\ No newline at end of file
+{{#if this.isTag}}
+ {{discourse-tag this.rowValue noHref=true count=this.item.count}}
+{{else}}
+ {{this.item.name}}
+{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/select-kit/addon/components/tag-row.js b/app/assets/javascripts/select-kit/addon/components/tag-row.js
index ec932fbbfbd..3801867edbb 100644
--- a/app/assets/javascripts/select-kit/addon/components/tag-row.js
+++ b/app/assets/javascripts/select-kit/addon/components/tag-row.js
@@ -1,5 +1,11 @@
+import discourseComputed from "discourse-common/utils/decorators";
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
export default SelectKitRowComponent.extend({
classNames: ["tag-row"],
+
+ @discourseComputed("item")
+ isTag(item) {
+ return item.id !== "no-tags" && item.id !== "all-tags";
+ },
});
diff --git a/app/assets/stylesheets/common/select-kit/category-drop.scss b/app/assets/stylesheets/common/select-kit/category-drop.scss
index d5d746e7f9d..4bc6735308e 100644
--- a/app/assets/stylesheets/common/select-kit/category-drop.scss
+++ b/app/assets/stylesheets/common/select-kit/category-drop.scss
@@ -10,6 +10,10 @@
}
.category-drop-header {
+ &[data-value=""] {
+ color: var(--primary-high);
+ }
+
&.is-none .selected-name {
color: inherit;
}
@@ -42,6 +46,22 @@
font-size: var(--font-down-1);
}
}
+
+ .category-drop-footer {
+ align-items: center;
+ border-top: 1px solid var(--primary-low);
+ display: flex;
+ font-size: var(--font-down-1);
+ height: 30px;
+ justify-content: space-between;
+ width: 100%;
+
+ a,
+ span {
+ color: var(--primary-high);
+ margin: 0 10px;
+ }
+ }
}
}
}
diff --git a/app/assets/stylesheets/common/select-kit/tag-drop.scss b/app/assets/stylesheets/common/select-kit/tag-drop.scss
index 382c3787321..e295d9ddb71 100644
--- a/app/assets/stylesheets/common/select-kit/tag-drop.scss
+++ b/app/assets/stylesheets/common/select-kit/tag-drop.scss
@@ -10,6 +10,10 @@
font-weight: 700;
}
}
+
+ .tag-drop-header {
+ color: var(--primary-high);
+ }
}
}
}
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index ec63117b18a..341cb6ef379 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -392,6 +392,8 @@ class CategoriesController < ApplicationController
categories = categories.where(parent_category_id: nil) if !include_subcategories
+ categories_count = categories.count
+
categories = categories.limit(limit || MAX_CATEGORIES_LIMIT)
Category.preload_user_fields!(guardian, categories)
@@ -409,18 +411,17 @@ class CategoriesController < ApplicationController
]
end
+ response = {
+ categories_count: categories_count,
+ categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
+ }
+
if include_ancestors
ancestors = Category.secured(guardian).ancestors_of(categories.map(&:id))
-
- render_json_dump(
- {
- categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
- ancestors: serialize_data(ancestors, SiteCategorySerializer, scope: guardian),
- },
- )
- else
- render_serialized(categories, SiteCategorySerializer, root: :categories, scope: guardian)
+ response[:ancestors] = serialize_data(ancestors, SiteCategorySerializer, scope: guardian)
end
+
+ render_json_dump(response)
end
private
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 11d6738c525..695ff8fbd7a 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1034,9 +1034,13 @@ en:
"15": "Drafts"
categories:
- all: "all categories"
- all_subcategories: "all"
- no_subcategory: "none"
+ categories_label: "categories"
+ subcategories_label: "subcategories"
+ all_subcategories: "all subcategories"
+ no_subcategories: "no subcategories"
+ remove_filter: "remove filter"
+ plus_more_count: "+%{count} more"
+ view_all: "view all"
category: "Category"
category_list: "Display category list"
reorder:
@@ -4353,8 +4357,10 @@ en:
tagging:
all_tags: "All tags"
other_tags: "Other Tags"
+ selector_tags: "tags"
selector_all_tags: "all tags"
selector_no_tags: "no tags"
+ selector_remove_filter: "remove filter"
tags: "Tags"
choose_for_topic: "optional tags"
choose_for_topic_required:
diff --git a/spec/system/discovery_breadcrumb_navigation_spec.rb b/spec/system/discovery_breadcrumb_navigation_spec.rb
index 161a34cf2f7..b9bbd83952c 100644
--- a/spec/system/discovery_breadcrumb_navigation_spec.rb
+++ b/spec/system/discovery_breadcrumb_navigation_spec.rb
@@ -61,7 +61,7 @@ describe "Navigating with breadcrumbs", type: :system do
expect(discovery.topic_list).to have_topic(c1_topic_tagged)
expect(discovery.topic_list).to have_topics(count: 2)
- expect(discovery.tag_drop).to have_selected_name("all tags")
+ expect(discovery.tag_drop).to have_selected_name("tags")
discovery.tag_drop.select_row_by_value(tag.name)
expect(discovery.topic_list).to have_topics(count: 1)
@@ -74,8 +74,8 @@ describe "Navigating with breadcrumbs", type: :system do
expect(discovery.topic_list).to have_topic(c3_topic_tagged)
expect(discovery.topic_list).to have_topics(count: 2)
- expect(discovery.subcategory_drop).to have_selected_name("none")
- expect(discovery.tag_drop).to have_selected_name("all tags")
+ expect(discovery.subcategory_drop).to have_selected_name("no subcategories")
+ expect(discovery.tag_drop).to have_selected_name("tags")
discovery.tag_drop.select_row_by_value(tag.name)
expect(page).to have_current_path(