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:
parent
e6fdfcdcd2
commit
f839fe5c8c
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue