From e5597cd1963d3ad1539dfd84053862a5e441578d Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 3 May 2024 12:39:45 -0500 Subject: [PATCH] DEV: Make edit sidebar categories modal load more results incrementally (#26761) --- .../edit-navigation-menu/categories-modal.gjs | 135 ++++++++++++++---- ...ting_sidebar_categories_navigation_spec.rb | 37 +++++ 2 files changed, 144 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/sidebar/edit-navigation-menu/categories-modal.gjs b/app/assets/javascripts/discourse/app/components/sidebar/edit-navigation-menu/categories-modal.gjs index fd90cb1f619..79e6d6bd95a 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/edit-navigation-menu/categories-modal.gjs +++ b/app/assets/javascripts/discourse/app/components/sidebar/edit-navigation-menu/categories-modal.gjs @@ -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 {