mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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 { concat, fn, get } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { gt, includes, not } from "truth-helpers";
|
import { gt, includes, not } from "truth-helpers";
|
||||||
import EditNavigationMenuModal from "discourse/components/sidebar/edit-navigation-menu/modal";
|
import EditNavigationMenuModal from "discourse/components/sidebar/edit-navigation-menu/modal";
|
||||||
@ -45,6 +46,19 @@ function findAncestors(categories) {
|
|||||||
return ancestors;
|
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 {
|
export default class extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service site;
|
@service site;
|
||||||
@ -60,18 +74,27 @@ export default class extends Component {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
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.processing = false;
|
||||||
this.setFilterAndMode("", "everything");
|
this.setFilterAndMode("", "everything");
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilteredCategories(categories) {
|
setFilteredCategories(categories) {
|
||||||
|
this.filteredCategories = categories;
|
||||||
const ancestors = findAncestors(categories);
|
const ancestors = findAncestors(categories);
|
||||||
const allCategories = categories.concat(ancestors).uniqBy((c) => c.id);
|
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(
|
this.filteredCategoriesGroupings = splitWhere(
|
||||||
Category.sortCategories(allCategories),
|
Category.sortCategories(allCategories),
|
||||||
(category) => category.parent_category_id === undefined
|
(category) => category.parent_category_id === undefined
|
||||||
@ -80,51 +103,69 @@ export default class extends Component {
|
|||||||
this.filteredCategoryIds = categories.map((c) => c.id);
|
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) {
|
async searchCategories(filter, mode) {
|
||||||
if (filter === "" && mode === "only-selected") {
|
if (filter === "" && mode === "only-selected") {
|
||||||
this.setFilteredCategories(
|
this.setFilteredCategories(
|
||||||
await Category.asyncFindByIds(this.selectedSidebarCategoryIds)
|
await Category.asyncFindByIds(this.selectedSidebarCategoryIds)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.loadedPage = null;
|
||||||
|
this.hasMorePages = false;
|
||||||
} else {
|
} else {
|
||||||
const { categories } = await Category.asyncSearch(filter, {
|
const { categories } = await Category.asyncSearch(filter, {
|
||||||
includeAncestors: true,
|
includeAncestors: true,
|
||||||
includeUncategorized: false,
|
includeUncategorized: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredFetchedCategories = categories.filter((c) => {
|
this.setFetchedCategories(mode, categories);
|
||||||
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.setFilteredCategories(filteredFetchedCategories);
|
this.loadedPage = 1;
|
||||||
|
this.hasMorePages = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFilterAndMode(newFilter, newMode) {
|
async setFilterAndMode(newFilter, newMode) {
|
||||||
this.filter = newFilter;
|
this.requestedFilter = newFilter;
|
||||||
this.mode = newMode;
|
this.requestedMode = newMode;
|
||||||
|
|
||||||
if (!this.processing) {
|
if (!this.processing) {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (
|
||||||
const filter = this.filter;
|
this.loadedFilter !== this.requestedFilter ||
|
||||||
const mode = this.mode;
|
this.loadedMode !== this.requestedMode
|
||||||
|
) {
|
||||||
|
const filter = this.requestedFilter;
|
||||||
|
const mode = this.requestedMode;
|
||||||
|
|
||||||
await this.searchCategories(filter, mode);
|
await this.searchCategories(filter, mode);
|
||||||
|
|
||||||
|
this.loadedFilter = filter;
|
||||||
|
this.loadedMode = mode;
|
||||||
this.initialLoad = false;
|
this.initialLoad = false;
|
||||||
|
|
||||||
if (filter === this.filter && mode === this.mode) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.processing = false;
|
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) {
|
debouncedSetFilterAndMode(filter, mode) {
|
||||||
discourseDebounce(this, this.setFilterAndMode, filter, mode, INPUT_DELAY);
|
discourseDebounce(this, this.setFilterAndMode, filter, mode, INPUT_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetFilter() {
|
resetFilter() {
|
||||||
this.debouncedSetFilterAndMode(this.filter, "everything");
|
this.debouncedSetFilterAndMode(this.requestedFilter, "everything");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
filterSelected() {
|
filterSelected() {
|
||||||
this.debouncedSetFilterAndMode(this.filter, "only-selected");
|
this.debouncedSetFilterAndMode(this.requestedFilter, "only-selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
filterUnselected() {
|
filterUnselected() {
|
||||||
this.debouncedSetFilterAndMode(this.filter, "only-unselected");
|
this.debouncedSetFilterAndMode(this.requestedFilter, "only-unselected");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onFilterInput(filter) {
|
onFilterInput(filter) {
|
||||||
this.debouncedSetFilterAndMode(filter.toLowerCase().trim(), this.mode);
|
this.debouncedSetFilterAndMode(
|
||||||
|
filter.toLowerCase().trim(),
|
||||||
|
this.requestedMode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -234,6 +312,7 @@ export default class extends Component {
|
|||||||
<div
|
<div
|
||||||
class="sidebar-categories-form__row"
|
class="sidebar-categories-form__row"
|
||||||
style={{borderColor (get categories "0.color") "left"}}
|
style={{borderColor (get categories "0.color") "left"}}
|
||||||
|
{{didInsert this.didInsert}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{{#each categories as |category|}}
|
{{#each categories as |category|}}
|
||||||
|
@ -225,6 +225,43 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
|
|||||||
expect(modal).to have_checkbox(category2_subcategory)
|
expect(modal).to have_checkbox(category2_subcategory)
|
||||||
end
|
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
|
describe "when max_category_nesting has been set to 3" do
|
||||||
before_all { SiteSetting.max_category_nesting = 3 }
|
before_all { SiteSetting.max_category_nesting = 3 }
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user