Collection: prevent multiple load calls within a very short time (#20528)

* debounce loadCollection calls

* update document collection context

* update media collection context

* remove duplicate request to reload

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add fadeIn animation to empty state to prevent flicker

* Update collection-default.element.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
This commit is contained in:
Mads Rasmussen
2025-10-23 13:15:54 +02:00
committed by GitHub
parent 4252cbc64b
commit bd6ab83712
5 changed files with 88 additions and 19 deletions

View File

@@ -15,7 +15,7 @@ import { UmbArrayState, UmbBasicState, UmbNumberState, UmbObjectState } from '@u
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbSelectionManager, UmbPaginationManager, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionManager, UmbPaginationManager, UmbDeprecation, debounce } from '@umbraco-cms/backoffice/utils';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -121,11 +121,11 @@ export class UmbDefaultCollectionContext<
})
.onReject(() => {
// TODO: Maybe this can be removed?
this.requestCollection();
this._requestCollection();
})
.onSubmit(() => {
// TODO: Maybe this can be removed?
this.requestCollection();
this._requestCollection();
})
.observeRouteBuilder((routeBuilder) => {
this.#workspacePathBuilder.setValue(routeBuilder);
@@ -248,16 +248,30 @@ export class UmbDefaultCollectionContext<
return this.manifest?.meta.noItemsLabel ?? this.#config?.noItemsLabel ?? '#collection_noItemsTitle';
}
/* debouncing the load collection method because multiple filters can be set at the same time
that will trigger multiple load calls with different filter arguments */
public loadCollection = debounce(() => this._requestCollection(), 100);
/**
* Requests the collection from the repository.
* @returns {*}
* @returns {Promise<void>}
* @deprecated Deprecated since v.17.0.0. Use `loadCollection` instead.
* @memberof UmbCollectionContext
*/
public async requestCollection() {
new UmbDeprecation({
removeInVersion: '19.0.0',
deprecated: 'requestCollection',
solution: 'Use .loadCollection method instead',
}).warn();
return this._requestCollection();
}
protected async _requestCollection() {
await this._init;
if (!this._configured) this._configure();
if (!this._repository) throw new Error(`Missing repository for ${this._manifest}`);
this._loading.setValue(true);
@@ -281,7 +295,7 @@ export class UmbDefaultCollectionContext<
*/
public setFilter(filter: Partial<FilterModelType>) {
this._filter.setValue({ ...this._filter.getValue(), ...filter });
this.requestCollection();
this.loadCollection();
}
public updateFilter(filter: Partial<FilterModelType>) {
@@ -312,7 +326,7 @@ export class UmbDefaultCollectionContext<
const items = this._items.getValue();
const hasItem = items.some((item) => item.unique === event.getUnique());
if (hasItem) {
this.requestCollection();
this._requestCollection();
}
};
@@ -324,7 +338,7 @@ export class UmbDefaultCollectionContext<
const entityType = entityContext.getEntityType();
if (unique === event.getUnique() && entityType === event.getEntityType()) {
this.requestCollection();
this._requestCollection();
}
};

View File

@@ -23,7 +23,6 @@ umbExtensionsRegistry.register(manifest);
@customElement('umb-collection-default')
export class UmbCollectionDefaultElement extends UmbLitElement {
//
#collectionContext?: UmbDefaultCollectionContext;
@state()
@@ -33,23 +32,43 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
private _hasItems = false;
@state()
private _isDoneLoading = false;
private _emptyLabel?: string;
@state()
private _emptyLabel?: string;
private _initialLoadDone = false;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, async (context) => {
this.#collectionContext = context;
this.#observeIsLoading();
this.#observeCollectionRoutes();
this.#observeTotalItems();
this.#getEmptyStateLabel();
await this.#collectionContext?.requestCollection();
this._isDoneLoading = true;
this.#collectionContext?.loadCollection();
});
}
#observeIsLoading() {
if (!this.#collectionContext) return;
let hasBeenLoading = false;
this.observe(
this.#collectionContext.loading,
(isLoading) => {
// We need to know when the initial loading has been done, to not show the empty state before that.
// We can't just check if there are items, because there might be none.
// So we check if it has been loading, and then when it stops loading we know the initial load is done.
if (isLoading) {
hasBeenLoading = true;
} else if (hasBeenLoading) {
this._initialLoadDone = true;
}
},
'umbCollectionIsLoadingObserver',
);
}
#observeCollectionRoutes() {
if (!this.#collectionContext) return;
@@ -106,7 +125,7 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
}
#renderEmptyState() {
if (!this._isDoneLoading) return nothing;
if (!this._initialLoadDone) return nothing;
return html`
<div id="empty-state" class="uui-text">
@@ -138,12 +157,20 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
height: 80%;
align-content: center;
text-align: center;
opacity: 0;
animation: fadeIn 200ms 200ms forwards;
}
router-slot {
width: 100%;
height: 100%;
}
@keyframes fadeIn {
100% {
opacity: 100%;
}
}
`,
];
}

View File

@@ -4,6 +4,7 @@ import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { UmbDeprecation } from '@umbraco-cms/backoffice/utils';
export class UmbDocumentCollectionContext extends UmbDefaultCollectionContext<
UmbDocumentCollectionItemModel,
@@ -35,9 +36,25 @@ export class UmbDocumentCollectionContext extends UmbDefaultCollectionContext<
);
}
public override async requestCollection() {
/**
* Requests the collection from the repository.
* @returns {Promise<void>}
* @deprecated Deprecated since v.17.0.0. Use `loadCollection` instead.
* @memberof UmbDocumentCollectionContext
*/
public override async requestCollection(): Promise<void> {
new UmbDeprecation({
removeInVersion: '19.0.0',
deprecated: 'requestCollection',
solution: 'Use .loadCollection method instead',
}).warn();
return this._requestCollection();
}
protected override async _requestCollection() {
await this.observe(this.#displayCultureObservable)?.asPromise();
await super.requestCollection();
await super._requestCollection();
}
}

View File

@@ -5,6 +5,7 @@ import type { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbDeprecation } from '@umbraco-cms/backoffice/utils';
export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
UmbMediaCollectionItemModel,
UmbMediaCollectionFilterModel
@@ -51,9 +52,20 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
/**
* Requests the collection from the repository.
* @returns {Promise<void>}
* @memberof UmbCollectionContext
* @deprecated Deprecated since v.17.0.0. Use `loadCollection` instead.
* @memberof UmbMediaCollectionContext
*/
public override async requestCollection() {
public override async requestCollection(): Promise<void> {
new UmbDeprecation({
removeInVersion: '19.0.0',
deprecated: 'requestCollection',
solution: 'Use .loadCollection method instead',
}).warn();
return this._requestCollection();
}
protected override async _requestCollection() {
await this._init;
if (!this._configured) this._configure();

View File

@@ -62,7 +62,6 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
async #onComplete(event: Event) {
event.preventDefault();
this._progress = -1;
this.#collectionContext?.requestCollection();
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
if (!eventContext) {