FIX: Fetch categories for "+subcategories" option (#26622)

Selecting the +subcategories option does not work sometimes when "lazy
load categories" is enabled because the subcategories may not be
fetched. This ensures that subcategories are loaded by requesting them
before being used.
This commit is contained in:
Bianca Nenciu 2024-04-25 16:47:45 +03:00 committed by GitHub
parent 2215fa0c8e
commit dbe923d26e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 89 additions and 40 deletions

View File

@ -399,6 +399,7 @@ export default class Category extends RestModel {
include_ancestors: opts.includeAncestors, include_ancestors: opts.includeAncestors,
prioritized_category_id: opts.prioritizedCategoryId, prioritized_category_id: opts.prioritizedCategoryId,
limit: opts.limit, limit: opts.limit,
page: opts.page,
}; };
const result = (CATEGORY_ASYNC_SEARCH_CACHE[JSON.stringify(data)] ||= const result = (CATEGORY_ASYNC_SEARCH_CACHE[JSON.stringify(data)] ||=

View File

@ -521,6 +521,7 @@ export default {
notification_level: null, notification_level: null,
background_url: null, background_url: null,
has_children: true, has_children: true,
subcategory_count: 2,
}, },
{ {
id: 1002, id: 1002,

View File

@ -111,8 +111,8 @@ export default class CategoryRow extends Component {
this.allowUncategorizedTopics || this.allowUncategorized, this.allowUncategorizedTopics || this.allowUncategorized,
hideParent: !!this.parentCategory, hideParent: !!this.parentCategory,
topicCount: this.topicCount, topicCount: this.topicCount,
subcategoryCount: this.args.item?.categories subcategoryCount: this.args.item?.category
? this.args.item.categories.length - 1 ? this.category.subcategory_count
: 0, : 0,
}) })
); );

View File

@ -72,40 +72,45 @@ export default MultiSelectComponent.extend({
// If there is a single match or an exact match and it has subcategories, // If there is a single match or an exact match and it has subcategories,
// add a row for selecting all subcategories // add a row for selecting all subcategories
if ( if (
categories.length === 1 || (categories.length === 1 ||
(categories.length > 0 && categories[0].name.localeCompare(filter) === 0) (categories.length > 0 &&
categories[0].name.localeCompare(filter) === 0)) &&
categories[0].subcategory_count > 0
) { ) {
// Descendants may not be loaded if lazy loading is enabled. Search for categories.splice(
// subcategories will make sure these are loaded 1,
if (this.site.lazy_load_categories) { 0,
await Category.asyncSearch("", { EmberObject.create({
parentCategoryId: categories[0].id, // This is just a hack to ensure the IDs are unique, but ensure
}); // that parseInt still returns a valid ID in order to generate the
} // label
id: `${categories[0].id}+subcategories`,
if (categories[0].descendants.length > 1) { category: categories[0],
categories.splice( })
1, );
0,
EmberObject.create({
// This is just a hack to ensure the IDs are unique, but ensure
// that parseInt still returns a valid ID in order to generate the
// label
id: `${categories[0].id}+subcategories`,
categories: categories[0].descendants,
})
);
}
} }
return categories; return categories;
}, },
select(value, item) { async select(value, item) {
if (item.categories) { // item is usually a category, but if the "category" property is set, then
// it is the special row for selecting all subcategories
if (item.category) {
if (this.site.lazy_load_categories) {
// Descendants may not be loaded if lazy loading is enabled. Searching
// for subcategories will make sure these are loaded
for (let page = 1, categories = [null]; categories.length > 0; page++) {
categories = await Category.asyncSearch("", {
parentCategoryId: item.category.id,
page,
});
}
}
this.selectKit.change( this.selectKit.change(
makeArray(this.value).concat(item.categories.mapBy("id")), makeArray(this.value).concat(item.category.descendants.mapBy("id")),
makeArray(this.selectedContent).concat(item.categories) makeArray(this.selectedContent).concat(item.category.descendants)
); );
} else { } else {
this._super(value, item); this._super(value, item);

View File

@ -362,7 +362,15 @@ class CategoriesController < ApplicationController
prioritized_category_id = params[:prioritized_category_id].to_i if params[ prioritized_category_id = params[:prioritized_category_id].to_i if params[
:prioritized_category_id :prioritized_category_id
].present? ].present?
limit = params[:limit].to_i.clamp(1, MAX_CATEGORIES_LIMIT) if params[:limit].present? limit =
(
if params[:limit].present?
params[:limit].to_i.clamp(1, MAX_CATEGORIES_LIMIT)
else
MAX_CATEGORIES_LIMIT
end
)
page = [1, params[:page].to_i].max
categories = Category.secured(guardian) categories = Category.secured(guardian)
@ -404,7 +412,8 @@ class CategoriesController < ApplicationController
) )
.joins("LEFT JOIN topics t on t.id = categories.topic_id") .joins("LEFT JOIN topics t on t.id = categories.topic_id")
.select("categories.*, t.slug topic_slug") .select("categories.*, t.slug topic_slug")
.limit(limit || MAX_CATEGORIES_LIMIT) .limit(limit)
.offset((page - 1) * limit)
if Site.preloaded_category_custom_fields.present? if Site.preloaded_category_custom_fields.present?
Category.preload_custom_fields(categories, Site.preloaded_category_custom_fields) Category.preload_custom_fields(categories, Site.preloaded_category_custom_fields)

View File

@ -226,7 +226,8 @@ class Category < ActiveRecord::Base
:subcategory_ids, :subcategory_ids,
:subcategory_list, :subcategory_list,
:notification_level, :notification_level,
:has_children :has_children,
:subcategory_count
# Allows us to skip creating the category definition topic in tests. # Allows us to skip creating the category definition topic in tests.
attr_accessor :skip_category_definition attr_accessor :skip_category_definition
@ -244,13 +245,9 @@ class Category < ActiveRecord::Base
Category.topic_create_allowed(guardian).where(id: category_ids).pluck(:id).to_set Category.topic_create_allowed(guardian).where(id: category_ids).pluck(:id).to_set
end end
# Categories with children # Load subcategory counts (used to fill has_children property)
with_children = subcategory_count =
Category Category.secured(guardian).where.not(parent_category_id: nil).group(:parent_category_id).count
.secured(guardian)
.where(parent_category_id: category_ids)
.pluck(:parent_category_id)
.to_set
# Update category attributes # Update category attributes
categories.each do |category| categories.each do |category|
@ -259,7 +256,9 @@ class Category < ActiveRecord::Base
category.permission = CategoryGroup.permission_types[:full] if guardian.is_admin? || category.permission = CategoryGroup.permission_types[:full] if guardian.is_admin? ||
allowed_topic_create_ids&.include?(category[:id]) allowed_topic_create_ids&.include?(category[:id])
category.has_children = with_children.include?(category[:id]) category.has_children = subcategory_count.key?(category[:id])
category.subcategory_count = subcategory_count[category[:id]] if category.has_children
end end
end end

View File

@ -20,6 +20,7 @@ class BasicCategorySerializer < ApplicationSerializer
:can_edit, :can_edit,
:topic_template, :topic_template,
:has_children, :has_children,
:subcategory_count,
:sort_order, :sort_order,
:sort_ascending, :sort_ascending,
:show_subcategory_list, :show_subcategory_list,

View File

@ -84,6 +84,12 @@
"null" "null"
] ]
}, },
"subcategory_count": {
"type": [
"integer",
"null"
]
},
"sort_order": { "sort_order": {
"type": [ "type": [
"string", "string",
@ -287,6 +293,7 @@
"can_edit", "can_edit",
"topic_template", "topic_template",
"has_children", "has_children",
"subcategory_count",
"sort_order", "sort_order",
"sort_ascending", "sort_ascending",
"show_subcategory_list", "show_subcategory_list",

View File

@ -78,6 +78,12 @@
"has_children": { "has_children": {
"type": "boolean" "type": "boolean"
}, },
"subcategory_count": {
"type": [
"integer",
"null"
]
},
"sort_order": { "sort_order": {
"type": [ "type": [
"string", "string",
@ -194,6 +200,7 @@
"can_edit", "can_edit",
"topic_template", "topic_template",
"has_children", "has_children",
"subcategory_count",
"sort_order", "sort_order",
"sort_ascending", "sort_ascending",
"show_subcategory_list", "show_subcategory_list",

View File

@ -87,6 +87,12 @@
"null" "null"
] ]
}, },
"subcategory_count": {
"type": [
"integer",
"null"
]
},
"sort_order": { "sort_order": {
"type": [ "type": [
"string", "string",
@ -291,6 +297,7 @@
"topic_template", "topic_template",
"form_template_ids", "form_template_ids",
"has_children", "has_children",
"subcategory_count",
"sort_order", "sort_order",
"sort_ascending", "sort_ascending",
"show_subcategory_list", "show_subcategory_list",

View File

@ -603,6 +603,12 @@
"has_children": { "has_children": {
"type": "boolean" "type": "boolean"
}, },
"subcategory_count": {
"type": [
"integer",
"null"
]
},
"sort_order": { "sort_order": {
"type": [ "type": [
"string", "string",
@ -740,6 +746,7 @@
"notification_level", "notification_level",
"topic_template", "topic_template",
"has_children", "has_children",
"subcategory_count",
"sort_order", "sort_order",
"sort_ascending", "sort_ascending",
"show_subcategory_list", "show_subcategory_list",

View File

@ -1115,6 +1115,7 @@ RSpec.describe CategoriesController do
expect(serialized["notification_level"]).to eq(CategoryUser.default_notification_level) expect(serialized["notification_level"]).to eq(CategoryUser.default_notification_level)
expect(serialized["permission"]).to eq(nil) expect(serialized["permission"]).to eq(nil)
expect(serialized["has_children"]).to eq(false) expect(serialized["has_children"]).to eq(false)
expect(serialized["subcategory_count"]).to eq(nil)
end end
it "does not return hidden category" do it "does not return hidden category" do
@ -1168,6 +1169,7 @@ RSpec.describe CategoriesController do
expect(category["notification_level"]).to eq(NotificationLevels.all[:regular]) expect(category["notification_level"]).to eq(NotificationLevels.all[:regular])
expect(category["permission"]).to eq(CategoryGroup.permission_types[:full]) expect(category["permission"]).to eq(CategoryGroup.permission_types[:full])
expect(category["has_children"]).to eq(true) expect(category["has_children"]).to eq(true)
expect(category["subcategory_count"]).to eq(1)
end end
context "with a read restricted child category" do context "with a read restricted child category" do
@ -1179,6 +1181,7 @@ RSpec.describe CategoriesController do
get "/categories/find.json", params: { ids: [category.id] } get "/categories/find.json", params: { ids: [category.id] }
category = response.parsed_body["categories"].first category = response.parsed_body["categories"].first
expect(category["has_children"]).to eq(true) expect(category["has_children"]).to eq(true)
expect(category["subcategory_count"]).to eq(1)
end end
it "indicates to a normal user that the category has no child" do it "indicates to a normal user that the category has no child" do
@ -1187,6 +1190,7 @@ RSpec.describe CategoriesController do
get "/categories/find.json", params: { ids: [category.id] } get "/categories/find.json", params: { ids: [category.id] }
category = response.parsed_body["categories"].first category = response.parsed_body["categories"].first
expect(category["has_children"]).to eq(false) expect(category["has_children"]).to eq(false)
expect(category["subcategory_count"]).to eq(nil)
end end
end end
end end
@ -1415,6 +1419,7 @@ RSpec.describe CategoriesController do
expect(category["notification_level"]).to eq(NotificationLevels.all[:regular]) expect(category["notification_level"]).to eq(NotificationLevels.all[:regular])
expect(category["permission"]).to eq(CategoryGroup.permission_types[:full]) expect(category["permission"]).to eq(CategoryGroup.permission_types[:full])
expect(category["has_children"]).to eq(true) expect(category["has_children"]).to eq(true)
expect(category["subcategory_count"]).to eq(1)
end end
end end
end end