From 8dbcfef3fd2d7d05554343901fae690bafb3b518 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 6 Mar 2024 20:14:36 +0200 Subject: [PATCH] FEATURE: Add "+ subcategories" option back (#26035) This option was introduced at some point in the past, but was removed during the work necessary to make Discourse work with a large number of categories. Follow up to commit 2e68ead45b4cb94fa65c46694716c0ccf25c949d. --- .../discourse/app/helpers/category-link.js | 15 +++-- .../discourse/app/models/category.js | 11 ++++ .../select-kit/category-selector-test.js | 57 +++++++++++++++++ .../addon/components/category-row.gjs | 2 +- .../addon/components/category-selector.js | 62 ++++++++++++------- .../common/select-kit/category-selector.scss | 5 ++ config/locales/client.en.yml | 3 + 7 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/integration/components/select-kit/category-selector-test.js diff --git a/app/assets/javascripts/discourse/app/helpers/category-link.js b/app/assets/javascripts/discourse/app/helpers/category-link.js index b6b0979120c..3963f428e26 100644 --- a/app/assets/javascripts/discourse/app/helpers/category-link.js +++ b/app/assets/javascripts/discourse/app/helpers/category-link.js @@ -135,10 +135,10 @@ export function defaultCategoryLinkRenderer(category, opts) { dataAttributes += ` data-parent-category-id="${parentCat.id}"`; } - html += `${I18n.t( + "category_row.subcategory_count", + { count: opts.subcategoryCount } + )}`; + } + if (href) { href = ` href="${href}" `; } diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index b3b802dd0ee..e68af1d6031 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -464,6 +464,17 @@ export default class Category extends RestModel { return [...(parentAncestors || []), this]; } + @discourseComputed("subcategories") + descendants() { + const descendants = [this]; + for (let i = 0; i < descendants.length; i++) { + if (descendants[i].subcategories) { + descendants.push(...descendants[i].subcategories); + } + } + return descendants; + } + @discourseComputed("parentCategory.level") level(parentLevel) { if (!parentLevel) { diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-selector-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-selector-test.js new file mode 100644 index 00000000000..e7697a6b7f3 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-selector-test.js @@ -0,0 +1,57 @@ +import { render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { module, test } from "qunit"; +import Category from "discourse/models/category"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import selectKit from "discourse/tests/helpers/select-kit-helper"; + +module( + "Integration | Component | select-kit/category-selector", + function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.set("subject", selectKit()); + }); + + test("with value", async function (assert) { + const category = Category.findById(1001); + const subcategory = Category.findById(1002); + this.set("value", [category, subcategory]); + + await render(hbs` + + `); + + assert.strictEqual(this.subject.header().value(), "1001,1002"); + assert.strictEqual( + this.subject.header().label(), + "Parent Category, Sub Category" + ); + }); + + test("has +subcategories row", async function (assert) { + this.set("value", []); + + await render(hbs` + + `); + await this.subject.expand(); + await this.subject.fillInFilter("Parent Category"); + + assert.equal(this.subject.rows().length, 2); + assert.equal( + this.subject.rowByIndex(0).el().innerText.replace("\n", " "), + "Parent Category × 95" + ); + assert.equal( + this.subject.rowByIndex(1).el().innerText.replace("\n", " "), + "Parent Category + 2 subcategories" + ); + }); + } +); diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.gjs b/app/assets/javascripts/select-kit/addon/components/category-row.gjs index 59a8c7fa494..a03f3b79035 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-row.gjs +++ b/app/assets/javascripts/select-kit/addon/components/category-row.gjs @@ -64,7 +64,7 @@ export default class CategoryRow extends Component { } get label() { - return this.args.item?.name; + return this.args.item?.name || this.args.item?.label; } get displayCategoryDescription() { diff --git a/app/assets/javascripts/select-kit/addon/components/category-selector.js b/app/assets/javascripts/select-kit/addon/components/category-selector.js index 70dccad7a1a..dc1dfe99e3b 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/category-selector.js @@ -1,5 +1,6 @@ -import { computed } from "@ember/object"; +import EmberObject, { computed } from "@ember/object"; import { mapBy } from "@ember/object/computed"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; import Category from "discourse/models/category"; import { makeArray } from "discourse-common/lib/helpers"; import CategoryRow from "select-kit/components/category-row"; @@ -51,34 +52,51 @@ export default MultiSelectComponent.extend({ }, async search(filter) { - if (!this.site.lazy_load_categories) { - return this._super(filter); + let categories; + if (this.site.lazy_load_categories) { + const rejectCategoryIds = new Set([ + ...this.categories.map((c) => c.id), + ...this.blockedCategories.map((c) => c.id), + ]); + + categories = await Category.asyncSearch(filter, { + includeUncategorized: + this.options?.allowUncategorized !== undefined + ? this.options.allowUncategorized + : this.selectKit.options.allowUncategorized, + rejectCategoryIds: Array.from(rejectCategoryIds), + }); + } else { + categories = this._super(filter); } - const rejectCategoryIds = new Set([ - ...this.categories.map((c) => c.id), - ...this.blockedCategories.map((c) => c.id), - ]); + // If there is a single match and it has subcategories, add a row for + // selecting all + if (categories.length === 1) { + const descendants = categories[0].descendants; + if (descendants.length > 1) { + categories.push( + EmberObject.create({ + label: categoryBadgeHTML(descendants[0], { + link: false, + recursive: true, + subcategoryCount: descendants.length - 1, + }), + categories: [...descendants], + }) + ); + } + } - return await Category.asyncSearch(filter, { - includeUncategorized: - this.options?.allowUncategorized !== undefined - ? this.options.allowUncategorized - : this.selectKit.options.allowUncategorized, - rejectCategoryIds: Array.from(rejectCategoryIds), - }); + return categories; }, select(value, item) { - if (item.multiCategory) { - const items = item.multiCategory.map((id) => - Category.findById(parseInt(id, 10)) + if (item.categories) { + this.selectKit.change( + makeArray(this.value).concat(item.categories.mapBy("id")), + makeArray(this.selectedContent).concat(item.categories) ); - - const newValues = makeArray(this.value).concat(items.map((i) => i.id)); - const newContent = makeArray(this.selectedContent).concat(items); - - this.selectKit.change(newValues, newContent); } else { this._super(value, item); } diff --git a/app/assets/stylesheets/common/select-kit/category-selector.scss b/app/assets/stylesheets/common/select-kit/category-selector.scss index c4b9169fdff..89d0b277195 100644 --- a/app/assets/stylesheets/common/select-kit/category-selector.scss +++ b/app/assets/stylesheets/common/select-kit/category-selector.scss @@ -11,6 +11,11 @@ .topic-count { margin-left: 0.25em; } + + .plus-subcategories { + font-size: var(--font-down-2); + margin-left: 0.25em; + } } .selected-choice-category { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d10bab68b49..11d6738c525 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2340,6 +2340,9 @@ en: loading: Loading… category_row: + subcategory_count: + one: "+ %{count} subcategory" + other: "+ %{count} subcategories" topic_count: one: "%{count} topic in this category" other: "%{count} topics in this category"