FEATURE: Add pagination to categories page (#23976)

When `lazy_load_categories` is enabled, the categories are no longer
preloaded in the `Site` object, but instead they are being requested
on a need basis.

The categories page still loaded all categories at once, which was not
ideal for sites with many categories because ti would take a lot of
time to build and parse the response.

This commit adds pagination to the categories page using the LoadMore
helper. As the user scrolls through the categories page, more categories
are requested from the server and appended to the page.

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Bianca Nenciu 2023-12-11 17:58:45 +02:00 committed by GitHub
parent af23fec835
commit 81b0420614
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 40 deletions

View File

@ -7,6 +7,7 @@ import CategoriesBoxes from "discourse/components/categories-boxes";
import CategoriesBoxesWithTopics from "discourse/components/categories-boxes-with-topics";
import CategoriesOnly from "discourse/components/categories-only";
import CategoriesWithFeaturedTopics from "discourse/components/categories-with-featured-topics";
import LoadMore from "discourse/components/load-more";
import PluginOutlet from "discourse/components/plugin-outlet";
import SubcategoriesWithFeaturedTopics from "discourse/components/subcategories-with-featured-topics";
@ -80,6 +81,18 @@ export default class CategoriesDisplay extends Component {
@connectorTagName="div"
@outletArgs={{hash categories=@categories topics=@topics}}
/>
<this.categoriesComponent @categories={{@categories}} @topics={{@topics}} />
{{#if this.siteSettings.lazy_load_categories}}
<LoadMore @selector=".category" @action={{@loadMore}}>
<this.categoriesComponent
@categories={{@categories}}
@topics={{@topics}}
/>
</LoadMore>
{{else}}
<this.categoriesComponent
@categories={{@categories}}
@topics={{@topics}}
/>
{{/if}}
</template>
}

View File

@ -6,18 +6,52 @@ import PreloadStore from "discourse/lib/preload-store";
import Category from "discourse/models/category";
import Site from "discourse/models/site";
import Topic from "discourse/models/topic";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
const MAX_CATEGORIES_LIMIT = 25;
const CategoryList = ArrayProxy.extend({
init() {
this.set("content", []);
this._super(...arguments);
this.set("content", []);
this.set("page", 1);
},
@bind
async loadMore() {
if (this.isLoading || this.lastPage) {
return;
}
this.set("isLoading", true);
const data = { page: this.page + 1, limit: MAX_CATEGORIES_LIMIT };
if (this.parentCategory) {
data.parent_category_id = this.parentCategory.id;
}
const result = await ajax("/categories.json", { data });
this.set("page", data.page);
result.category_list.categories.forEach((c) => {
const record = Site.current().updateCategory(c);
this.categories.pushObject(record);
});
this.set("isLoading", false);
if (result.category_list.categories.length === 0) {
this.set("lastPage", true);
}
const newCategoryList = CategoryList.categoriesFrom(this.store, result);
this.categories.pushObjects(newCategoryList.categories);
},
});
CategoryList.reopenClass({
categoriesFrom(store, result) {
const categories = CategoryList.create();
const categories = CategoryList.create({ store });
const list = Category.list();
let statPeriod = "all";
@ -55,6 +89,11 @@ CategoryList.reopenClass({
);
}
if (c.subcategories) {
// TODO: Not all subcategory_ids have been loaded
c.subcategories = c.subcategories?.filter(Boolean);
}
if (c.topics) {
c.topics = c.topics.map((t) => Topic.create(t));
}
@ -100,6 +139,7 @@ CategoryList.reopenClass({
`/categories.json?parent_category_id=${category.get("id")}`
).then((result) => {
return EmberObject.create({
store,
categories: this.categoriesFrom(store, result),
parentCategory: category,
});
@ -111,6 +151,7 @@ CategoryList.reopenClass({
return PreloadStore.getAndRemove("categories_list", getCategories).then(
(result) => {
return CategoryList.create({
store,
categories: this.categoriesFrom(store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,

View File

@ -1,4 +1,4 @@
import EmberObject, { action } from "@ember/object";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { hash } from "rsvp";
import { ajax } from "discourse/lib/ajax";
@ -79,46 +79,30 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
};
}
_findCategoriesAndTopics(filter) {
return hash({
wrappedCategoriesList: PreloadStore.getAndRemove("categories_list"),
async _findCategoriesAndTopics(filter) {
let result = await hash({
categoriesList: PreloadStore.getAndRemove("categories_list"),
topicsList: PreloadStore.getAndRemove("topic_list"),
}).then((response) => {
let { wrappedCategoriesList, topicsList } = response;
let categoriesList =
wrappedCategoriesList && wrappedCategoriesList.category_list;
let store = this.store;
});
if (categoriesList && topicsList) {
if (topicsList.topic_list?.top_tags) {
this.site.set("top_tags", topicsList.topic_list.top_tags);
}
return EmberObject.create({
categories: CategoryList.categoriesFrom(
this.store,
wrappedCategoriesList
),
topics: TopicList.topicsFrom(this.store, topicsList),
can_create_category: categoriesList.can_create_category,
can_create_topic: categoriesList.can_create_topic,
loadBefore: this._loadBefore(store),
});
}
if (result.categoriesList?.category_list && result.topicsList?.topic_list) {
result = { ...result.categoriesList, ...result.topicsList };
} else {
// Otherwise, return the ajax result
return ajax(`/categories_and_${filter}`).then((result) => {
if (result.topic_list?.top_tags) {
this.site.set("top_tags", result.topic_list.top_tags);
}
result = await ajax(`/categories_and_${filter}`);
}
return EmberObject.create({
categories: CategoryList.categoriesFrom(this.store, result),
topics: TopicList.topicsFrom(this.store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
loadBefore: this._loadBefore(store),
});
});
if (result.topic_list?.top_tags) {
this.site.set("top_tags", result.topic_list.top_tags);
}
return CategoryList.create({
store: this.store,
categories: CategoryList.categoriesFrom(this.store, result),
topics: TopicList.topicsFrom(this.store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
loadBefore: this._loadBefore(this.store),
});
}

View File

@ -34,6 +34,7 @@
@categories={{this.model.categories}}
@topics={{this.model.topics}}
@parentCategory={{this.model.parentCategory}}
@loadMore={{this.model.loadMore}}
/>
</div>

View File

@ -47,6 +47,7 @@ class CategoriesController < ApplicationController
include_topics: include_topics(parent_category),
include_subcategories: include_subcategories,
tag: params[:tag],
page: params[:page],
}
@category_list = CategoryList.new(guardian, category_options)
@ -407,6 +408,7 @@ class CategoriesController < ApplicationController
is_homepage: current_homepage == "categories",
parent_category_id: params[:parent_category_id],
include_topics: false,
page: params[:page],
}
topic_options = { per_page: CategoriesController.topics_per_page, no_definitions: true }

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class CategoryList
CATEGORIES_PER_PAGE = 25
include ActiveModel::Serialization
cattr_accessor :preloaded_topic_custom_fields
@ -134,6 +136,12 @@ class CategoryList
) if @options[:parent_category_id].present?
query = self.class.order_categories(query)
if SiteSetting.lazy_load_categories
page = [1, @options[:page].to_i].max
query = query.limit(CATEGORIES_PER_PAGE).offset((page - 1) * CATEGORIES_PER_PAGE)
end
query =
DiscoursePluginRegistry.apply_modifier(:category_list_find_categories_query, query, self)