DEV: Make edit sidebar categories modal load more results incrementally (#26761)

This commit is contained in:
Daniel Waterworth 2024-05-03 12:39:45 -05:00 committed by GitHub
parent d6b63309b0
commit e5597cd196
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 144 additions and 28 deletions

View File

@ -4,6 +4,7 @@ import { Input } from "@ember/component";
import { concat, fn, get } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
import { gt, includes, not } from "truth-helpers";
import EditNavigationMenuModal from "discourse/components/sidebar/edit-navigation-menu/modal";
@ -45,6 +46,19 @@ function findAncestors(categories) {
return ancestors;
}
function applyMode(mode, categories, selectedSidebarCategoryIds) {
return categories.filter((c) => {
switch (mode) {
case "everything":
return true;
case "only-selected":
return selectedSidebarCategoryIds.includes(c.id);
case "only-unselected":
return !selectedSidebarCategoryIds.includes(c.id);
}
});
}
export default class extends Component {
@service currentUser;
@service site;
@ -60,18 +74,27 @@ export default class extends Component {
constructor() {
super(...arguments);
this.observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
this.observer.disconnect();
this.loadMore();
}
},
{
threshold: 1.0,
}
);
this.processing = false;
this.setFilterAndMode("", "everything");
}
setFilteredCategories(categories) {
this.filteredCategories = categories;
const ancestors = findAncestors(categories);
const allCategories = categories.concat(ancestors).uniqBy((c) => c.id);
if (this.siteSettings.fixed_category_positions) {
allCategories.sort((a, b) => a.position - b.position);
}
this.filteredCategoriesGroupings = splitWhere(
Category.sortCategories(allCategories),
(category) => category.parent_category_id === undefined
@ -80,51 +103,69 @@ export default class extends Component {
this.filteredCategoryIds = categories.map((c) => c.id);
}
concatFilteredCategories(categories) {
this.setFilteredCategories(this.filteredCategories.concat(categories));
}
setFetchedCategories(mode, categories) {
this.setFilteredCategories(
applyMode(mode, categories, this.selectedSidebarCategoryIds)
);
}
concatFetchedCategories(mode, categories) {
this.concatFilteredCategories(
applyMode(mode, categories, this.selectedSidebarCategoryIds)
);
}
@action
didInsert(element) {
this.observer.disconnect();
this.observer.observe(element);
}
async searchCategories(filter, mode) {
if (filter === "" && mode === "only-selected") {
this.setFilteredCategories(
await Category.asyncFindByIds(this.selectedSidebarCategoryIds)
);
this.loadedPage = null;
this.hasMorePages = false;
} else {
const { categories } = await Category.asyncSearch(filter, {
includeAncestors: true,
includeUncategorized: false,
});
const filteredFetchedCategories = categories.filter((c) => {
switch (mode) {
case "everything":
return true;
case "only-selected":
return this.selectedSidebarCategoryIds.includes(c.id);
case "only-unselected":
return !this.selectedSidebarCategoryIds.includes(c.id);
}
});
this.setFetchedCategories(mode, categories);
this.setFilteredCategories(filteredFetchedCategories);
this.loadedPage = 1;
this.hasMorePages = true;
}
}
async setFilterAndMode(newFilter, newMode) {
this.filter = newFilter;
this.mode = newMode;
this.requestedFilter = newFilter;
this.requestedMode = newMode;
if (!this.processing) {
this.processing = true;
try {
while (true) {
const filter = this.filter;
const mode = this.mode;
while (
this.loadedFilter !== this.requestedFilter ||
this.loadedMode !== this.requestedMode
) {
const filter = this.requestedFilter;
const mode = this.requestedMode;
await this.searchCategories(filter, mode);
this.loadedFilter = filter;
this.loadedMode = mode;
this.initialLoad = false;
if (filter === this.filter && mode === this.mode) {
break;
}
}
} finally {
this.processing = false;
@ -132,28 +173,65 @@ export default class extends Component {
}
}
async loadMore() {
if (!this.processing && this.hasMorePages) {
this.processing = true;
try {
const page = this.loadedPage + 1;
const { categories } = await Category.asyncSearch(
this.requestedFilter,
{
includeAncestors: true,
includeUncategorized: false,
page,
}
);
this.loadedPage = page;
if (categories.length === 0) {
this.hasMorePages = false;
} else {
this.concatFetchedCategories(this.requestedMode, categories);
}
} finally {
this.processing = false;
}
if (
this.loadedFilter !== this.requestedFilter ||
this.loadedMode !== this.requestedMode
) {
await this.setFilterAndMode(this.requestedFilter, this.requestedMode);
}
}
}
debouncedSetFilterAndMode(filter, mode) {
discourseDebounce(this, this.setFilterAndMode, filter, mode, INPUT_DELAY);
}
@action
resetFilter() {
this.debouncedSetFilterAndMode(this.filter, "everything");
this.debouncedSetFilterAndMode(this.requestedFilter, "everything");
}
@action
filterSelected() {
this.debouncedSetFilterAndMode(this.filter, "only-selected");
this.debouncedSetFilterAndMode(this.requestedFilter, "only-selected");
}
@action
filterUnselected() {
this.debouncedSetFilterAndMode(this.filter, "only-unselected");
this.debouncedSetFilterAndMode(this.requestedFilter, "only-unselected");
}
@action
onFilterInput(filter) {
this.debouncedSetFilterAndMode(filter.toLowerCase().trim(), this.mode);
this.debouncedSetFilterAndMode(
filter.toLowerCase().trim(),
this.requestedMode
);
}
@action
@ -234,6 +312,7 @@ export default class extends Component {
<div
class="sidebar-categories-form__row"
style={{borderColor (get categories "0.color") "left"}}
{{didInsert this.didInsert}}
>
{{#each categories as |category|}}

View File

@ -225,6 +225,43 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
expect(modal).to have_checkbox(category2_subcategory)
end
context "when there are more categories than the page limit" do
around(:each) do |example|
search_calls = 0
spy =
CategoriesController.clone.prepend(
Module.new do
define_method :search do
search_calls += 1
super()
end
end,
)
@get_search_calls = lambda { search_calls }
stub_const(Object, :CategoriesController, spy) do
stub_const(CategoriesController, :MAX_CATEGORIES_LIMIT, 1) { example.run }
end
end
it "loads all the categories eventually" do
visit "/latest"
expect(sidebar).to have_categories_section
modal = sidebar.click_edit_categories_button
modal.filter("category")
expect(modal).to have_categories(
[category2, category2_subcategory, category, category_subcategory2, category_subcategory],
)
expect(@get_search_calls.call).to eq(6)
end
end
describe "when max_category_nesting has been set to 3" do
before_all { SiteSetting.max_category_nesting = 3 }