DEV: Make edit sidebar categories modal load more results incrementally (#26761)
This commit is contained in:
parent
d6b63309b0
commit
e5597cd196
|
@ -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|}}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
Loading…
Reference in New Issue