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:
parent
af23fec835
commit
81b0420614
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
@categories={{this.model.categories}}
|
||||
@topics={{this.model.topics}}
|
||||
@parentCategory={{this.model.parentCategory}}
|
||||
@loadMore={{this.model.loadMore}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue