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; + } + + +} 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(