UX: Show loading spinner when loading categories

This change ensures loading spinners are displayed when more categories
or subcategories are being loaded in the modal used for editing the
list of categories in the sidebar.
This commit is contained in:
Bianca Nenciu 2024-11-25 20:59:27 +02:00
parent e6fdfcdcd2
commit f839fe5c8c
1 changed files with 49 additions and 75 deletions

View File

@ -6,7 +6,7 @@ import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { TrackedSet } from "@ember-compat/tracked-built-ins"; import { TrackedSet } from "@ember-compat/tracked-built-ins";
import { eq, gt, has } from "truth-helpers"; import { eq, gt, has, not, or } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import EditNavigationMenuModal from "discourse/components/sidebar/edit-navigation-menu/modal"; import EditNavigationMenuModal from "discourse/components/sidebar/edit-navigation-menu/modal";
import borderColor from "discourse/helpers/border-color"; import borderColor from "discourse/helpers/border-color";
@ -70,41 +70,6 @@ function splitWhere(elements, f) {
}, []); }, []);
} }
// categories must be topologically sorted so that the parents appear before
// the children
function findPartialCategories(categories) {
const categoriesById = new Map(
categories.map((category) => [category.id, category])
);
const subcategoryCounts = new Map();
const subcategoryCountsRecursive = new Map();
const partialCategoryInfos = new Map();
for (const category of categories.slice().reverse()) {
const count = subcategoryCounts.get(category.parent_category_id) || 0;
subcategoryCounts.set(category.parent_category_id, count + 1);
const recursiveCount =
subcategoryCountsRecursive.get(category.parent_category_id) || 0;
subcategoryCountsRecursive.set(
category.parent_category_id,
recursiveCount + (subcategoryCountsRecursive.get(category.id) || 0) + 1
);
}
for (const [id, count] of subcategoryCounts) {
if (count === 5 && categoriesById.has(id)) {
partialCategoryInfos.set(id, {
level: categoriesById.get(id).level + 1,
offset: subcategoryCountsRecursive.get(id),
});
}
}
return partialCategoryInfos;
}
export default class SidebarEditNavigationMenuCategoriesModal extends Component { export default class SidebarEditNavigationMenuCategoriesModal extends Component {
@service currentUser; @service currentUser;
@service site; @service site;
@ -116,13 +81,14 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
selectedCategoryIds = new TrackedSet([ selectedCategoryIds = new TrackedSet([
...this.currentUser.sidebar_category_ids, ...this.currentUser.sidebar_category_ids,
]); ]);
@tracked loadAnotherPage = false;
@tracked loadingSubcategories = new TrackedSet();
selectedFilter = ""; selectedFilter = "";
selectedMode = "everything"; selectedMode = "everything";
loadedFilter; loadedFilter;
loadedMode; loadedMode;
loadedPage; loadedPage;
saving = false; saving = false;
loadAnotherPage = false;
unseenCategoryIdsChanged = false; unseenCategoryIdsChanged = false;
observer = new IntersectionObserver( observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
@ -143,6 +109,20 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
} }
recomputeGroupings() { recomputeGroupings() {
const categoriesById = new Map();
const categoriesCountByParentId = new Map();
this.fetchedCategories.forEach((category) => {
categoriesById.set(category.id, category);
if (category.parent_category_id !== undefined) {
categoriesCountByParentId.set(
category.parent_category_id,
(categoriesCountByParentId.get(category.parent_category_id) || 0) + 1
);
}
});
const categoriesWithShowMores = this.fetchedCategories.flatMap((el, i) => { const categoriesWithShowMores = this.fetchedCategories.flatMap((el, i) => {
const result = [{ type: "category", category: el }]; const result = [{ type: "category", category: el }];
@ -156,20 +136,21 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
if ( if (
!nextIsSibling && !nextIsSibling &&
!nextIsChild && !nextIsChild &&
this.partialCategoryInfos.has(elParentID) elParentID &&
categoriesCountByParentId.get(elParentID) <
categoriesById.get(elParentID).subcategory_count
) { ) {
const { level, offset } = this.partialCategoryInfos.get(elParentID);
result.push({ result.push({
type: "show-more", type: "show-more",
id: elParentID, id: elParentID,
level, level: el.level,
offset, offset: categoriesCountByParentId.get(elParentID),
loading: false,
}); });
} }
return result; return result;
}, []); });
this.fetchedCategoriesGroupings = splitWhere( this.fetchedCategoriesGroupings = splitWhere(
categoriesWithShowMores, categoriesWithShowMores,
@ -180,7 +161,6 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
setFetchedCategories(categories) { setFetchedCategories(categories) {
this.fetchedCategories = categories; this.fetchedCategories = categories;
this.partialCategoryInfos = findPartialCategories(categories);
this.recomputeGroupings(); this.recomputeGroupings();
} }
@ -199,18 +179,10 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
categories = [...this.fetchedCategories.slice(index), ...categories]; categories = [...this.fetchedCategories.slice(index), ...categories];
} }
this.partialCategoryInfos = new Map([
...this.partialCategoryInfos,
...findPartialCategories(categories),
]);
this.recomputeGroupings(); this.recomputeGroupings();
} }
substituteInFetchedCategories(id, subcategories, offset) { substituteInFetchedCategories(id, subcategories) {
this.partialCategoryInfos.delete(id);
this.recomputeGroupings();
if (subcategories.length !== 0) { if (subcategories.length !== 0) {
const index = const index =
this.fetchedCategories.findLastIndex( this.fetchedCategories.findLastIndex(
@ -222,18 +194,10 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
...subcategories, ...subcategories,
...this.fetchedCategories.slice(index), ...this.fetchedCategories.slice(index),
]; ];
this.partialCategoryInfos = new Map([
...this.partialCategoryInfos,
...findPartialCategories(subcategories),
]);
this.partialCategoryInfos.set(id, {
offset: offset + subcategories.length,
});
this.recomputeGroupings();
} }
this.recomputeGroupings();
this.loadingSubcategories.delete(id);
} }
@action @action
@ -296,7 +260,7 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
opts opts
); );
this.substituteInFetchedCategories(id, subcategories, offset); this.substituteInFetchedCategories(id, subcategories);
} }
} else { } else {
// The shown categories are stale, refresh everything // The shown categories are stale, refresh everything
@ -327,6 +291,7 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
@action @action
async loadSubcategories(id, offset) { async loadSubcategories(id, offset) {
this.loadingSubcategories.add(id);
this.subcategoryLoadList.push({ id, offset }); this.subcategoryLoadList.push({ id, offset });
this.debouncedSendRequest(); this.debouncedSendRequest();
} }
@ -428,11 +393,7 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
class="sidebar__edit-navigation-menu__categories-modal" class="sidebar__edit-navigation-menu__categories-modal"
> >
<form class="sidebar-categories-form"> <form class="sidebar-categories-form">
{{#if this.initialLoad}} {{#if (not this.initialLoad)}}
<div class="sidebar-categories-form__loading">
{{loadingSpinner size="small"}}
</div>
{{else}}
{{#each this.fetchedCategoriesGroupings as |categories|}} {{#each this.fetchedCategoriesGroupings as |categories|}}
<div <div
style={{borderColor (get categories "0.category.color") "left"}} style={{borderColor (get categories "0.category.color") "left"}}
@ -491,11 +452,19 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
<label class="sidebar-categories-form__category-label"> <label class="sidebar-categories-form__category-label">
<div class="sidebar-categories-form__category-wrapper"> <div class="sidebar-categories-form__category-wrapper">
<div class="sidebar-categories-form__category-badge"> <div class="sidebar-categories-form__category-badge">
<DButton {{#if (has this.loadingSubcategories c.id)}}
@label="sidebar.categories_form_modal.show_more" {{loadingSpinner size="small"}}
@action={{fn this.loadSubcategories c.id c.offset}} {{else}}
class="btn-flat" <DButton
/> @label="sidebar.categories_form_modal.show_more"
@action={{fn
this.loadSubcategories
c.id
c.offset
}}
class="btn-flat"
/>
{{/if}}
</div> </div>
</div> </div>
</label> </label>
@ -509,6 +478,11 @@ export default class SidebarEditNavigationMenuCategoriesModal extends Component
</div> </div>
{{/each}} {{/each}}
{{/if}} {{/if}}
{{#if (or this.initialLoad this.loadAnotherPage)}}
<div class="sidebar-categories-form__loading">
{{loadingSpinner}}
</div>
{{/if}}
</form> </form>
</EditNavigationMenuModal> </EditNavigationMenuModal>
</template> </template>