FIX: Show "no category" in category-chooser (#25917)

CategoryChooser component usually displays just categories, but
sometimes it can show two none values: a "no category" or Uncategorized.
This commit makes sure that these are rendered correctly.

The problem was that the "none" item was automatically inserted in the
list of options, but that should not always happen. Toggling option
`autoInsertNoneItem` requires setting `none` too.
This commit is contained in:
Bianca Nenciu 2024-02-29 13:48:20 +02:00 committed by GitHub
parent 0bb492c6b6
commit e74a9efee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 97 additions and 18 deletions

View File

@ -19,13 +19,13 @@
<label>{{i18n "category.parent"}}</label>
<CategoryChooser
@value={{this.category.parent_category_id}}
@categories={{this.parentCategories}}
@allowSubCategories={{true}}
@allowRestrictedCategories={{true}}
@onChange={{action (mut this.category.parent_category_id)}}
@options={{hash
allowUncategorized=false
excludeCategoryId=this.category.id
autoInsertNoneItem=true
none=true
}}
/>

View File

@ -176,6 +176,25 @@ export default class Category extends RestModel {
return category;
}
static async asyncFindBySlugPath(slugPath, opts = {}) {
const data = { slug_path: slugPath };
if (opts.includePermissions) {
data.include_permissions = true;
}
const result = await ajax("/categories/find", { data });
const categories = result["categories"].map((category) => {
category = Site.current().updateCategory(category);
if (opts.includePermissions) {
category.setupGroupsAndPermissions();
}
return category;
});
return categories[categories.length - 1];
}
static async asyncFindBySlugPathWithID(slugPathWithID) {
const result = await ajax("/categories/find", {
data: { slug_path_with_id: slugPathWithID },

View File

@ -7,11 +7,9 @@ export default DiscourseRoute.extend({
router: service(),
model(params) {
return Category.reloadCategoryWithPermissions(
params,
this.store,
this.site
);
return this.site.lazy_load_categories
? Category.asyncFindBySlugPath(params.slug, { includePermissions: true })
: Category.reloadCategoryWithPermissions(params, this.store, this.site);
},
afterModel(model) {

View File

@ -145,6 +145,36 @@ acceptance("Category Edit", function (needs) {
assert.deepEqual(removePayload.allowed_tag_groups, []);
});
test("Editing parent category (disabled Uncategorized)", async function (assert) {
this.siteSettings.allow_uncategorized_topics = false;
await visit("/c/bug/edit");
const categoryChooser = selectKit(".category-chooser");
await categoryChooser.expand();
await categoryChooser.selectRowByValue(6);
await categoryChooser.expand();
const names = [...categoryChooser.rows()].map((row) => row.dataset.name);
assert.ok(names.includes("(no category)"));
assert.notOk(names.includes("Uncategorized"));
});
test("Editing parent category (enabled Uncategorized)", async function (assert) {
this.siteSettings.allow_uncategorized_topics = true;
await visit("/c/bug/edit");
const categoryChooser = selectKit(".category-chooser");
await categoryChooser.expand();
await categoryChooser.selectRowByValue(6);
await categoryChooser.expand();
const names = [...categoryChooser.rows()].map((row) => row.dataset.name);
assert.ok(names.includes("(no category)"));
assert.notOk(names.includes("Uncategorized"));
});
test("Index Route", async function (assert) {
await visit("/c/bug/edit");
assert.strictEqual(

View File

@ -12,12 +12,13 @@ import ComboBoxComponent from "select-kit/components/combo-box";
export default ComboBoxComponent.extend({
pluginApiIdentifiers: ["category-chooser"],
classNames: ["category-chooser"],
allowUncategorizedTopics: setting("allow_uncategorized_topics"),
allowUncategorized: setting("allow_uncategorized_topics"),
fixedCategoryPositionsOnCreate: setting("fixed_category_positions_on_create"),
selectKitOptions: {
filterable: true,
allowUncategorized: false,
allowUncategorized: "allowUncategorized",
autoInsertNoneItem: false,
allowSubCategories: true,
permissionType: PermissionType.FULL,
excludeCategoryId: null,
@ -30,6 +31,7 @@ export default ComboBoxComponent.extend({
if (
this.site.lazy_load_categories &&
this.value &&
!Category.hasAsyncFoundAll([this.value])
) {
// eslint-disable-next-line no-console
@ -54,10 +56,7 @@ export default ComboBoxComponent.extend({
I18n.t(isString ? this.selectKit.options.none : "category.none")
)
);
} else if (
this.allowUncategorizedTopics ||
this.selectKit.options.allowUncategorized
) {
} else if (this.selectKit.options.allowUncategorized) {
return Category.findUncategorized();
} else {
const defaultCategoryId = parseInt(
@ -94,8 +93,10 @@ export default ComboBoxComponent.extend({
search(filter) {
if (this.site.lazy_load_categories) {
return Category.asyncSearch(this._normalize(filter), {
scopedCategoryId: this.selectKit.options?.scopedCategoryId,
prioritizedCategoryId: this.selectKit.options?.prioritizedCategoryId,
includeUncategorized: this.selectKit.options.allowUncategorized,
rejectCategoryIds: [this.selectKit.options.excludeCategoryId],
scopedCategoryId: this.selectKit.options.scopedCategoryId,
prioritizedCategoryId: this.selectKit.options.prioritizedCategoryId,
});
}

View File

@ -24,6 +24,7 @@ export default ComboBoxComponent.extend({
navigateToEdit: false,
editingCategory: false,
editingCategoryTab: null,
allowUncategorized: setting("allow_uncategorized_topics"),
selectKitOptions: {
filterable: true,
@ -40,7 +41,7 @@ export default ComboBoxComponent.extend({
displayCategoryDescription: "displayCategoryDescription",
headerComponent: "category-drop/category-drop-header",
parentCategory: false,
allowUncategorized: setting("allow_uncategorized_topics"),
allowUncategorized: "allowUncategorized",
},
modifyComponentForRow() {

View File

@ -303,9 +303,17 @@ class CategoriesController < ApplicationController
def find
categories = []
serializer = params[:include_permissions] ? CategorySerializer : SiteCategorySerializer
if params[:ids].present?
categories = Category.secured(guardian).where(id: params[:ids])
elsif params[:slug_path].present?
category = Category.find_by_slug_path(params[:slug_path].split("/"))
raise Discourse::NotFound if category.blank?
guardian.ensure_can_see!(category)
ancestors = Category.secured(guardian).with_ancestors(category.id).where.not(id: category.id)
categories = [*ancestors, category]
elsif params[:slug_path_with_id].present?
category = Category.find_by_slug_path_with_id(params[:slug_path_with_id])
raise Discourse::NotFound if category.blank?
@ -319,7 +327,7 @@ class CategoriesController < ApplicationController
Category.preload_user_fields!(guardian, categories)
render_serialized(categories, SiteCategorySerializer, root: :categories, scope: guardian)
render_serialized(categories, serializer, root: :categories, scope: guardian)
end
def search
@ -333,8 +341,12 @@ class CategoriesController < ApplicationController
true
end
)
select_category_ids = params[:select_category_ids].presence
reject_category_ids = params[:reject_category_ids].presence
if params[:select_category_ids].is_a?(Array)
select_category_ids = params[:select_category_ids].map(&:presence)
end
if params[:reject_category_ids].is_a?(Array)
reject_category_ids = params[:reject_category_ids].map(&:presence)
end
include_subcategories =
if params[:include_subcategories].present?
ActiveModel::Type::Boolean.new.cast(params[:include_subcategories])

View File

@ -1217,6 +1217,12 @@ RSpec.describe CategoriesController do
expect(response.parsed_body["categories"].size).to eq(1)
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly("Foo")
end
it "works with empty categories list" do
get "/categories/search.json", params: { select_category_ids: [""] }
expect(response.parsed_body["categories"].size).to eq(0)
end
end
context "with reject_category_ids" do
@ -1230,6 +1236,18 @@ RSpec.describe CategoriesController do
"Foobar",
)
end
it "works with empty categories list" do
get "/categories/search.json", params: { reject_category_ids: [""] }
expect(response.parsed_body["categories"].size).to eq(4)
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
"Uncategorized",
"Foo",
"Foobar",
"Notfoo",
)
end
end
context "with include_subcategories" do