Merge remote-tracking branch 'origin/main' into feature/new-models-20231107

# Conflicts:
#	src/mocks/data/user.data.ts
This commit is contained in:
Niels Lyngsø
2023-11-07 16:14:54 +01:00
26 changed files with 546 additions and 83 deletions

View File

@@ -1,11 +1,13 @@
import { UmbEntityData } from './entity.data.js';
import { umbUserGroupData } from './user-group.data.js';
import { arrayFilter, stringFilter, queryFilter } from './utils.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import {
CreateUserRequestModel,
CreateUserResponseModel,
InviteUserRequestModel,
PagedUserResponseModel,
UpdateUserGroupsOnUserRequestModel,
UserItemResponseModel,
UserResponseModel,
@@ -19,6 +21,10 @@ const createUserItem = (item: UserResponseModel): UserItemResponseModel => {
};
};
const userGroupFilter = (filterOptions: any, item: UserResponseModel) => arrayFilter(filterOptions.userGroupIds, item.userGroupIds);
const userStateFilter = (filterOptions: any, item: UserResponseModel) => stringFilter(filterOptions.userStates, item.state);
const userQueryFilter = (filterOptions: any, item: UserResponseModel) => queryFilter(filterOptions.filter, item.name);
// Temp mocked database
class UmbUserData extends UmbEntityData<UserResponseModel> {
constructor(data: UserResponseModel[]) {
@@ -139,6 +145,11 @@ class UmbUserData extends UmbEntityData<UserResponseModel> {
});
}
/**
* Invites a user
* @param {InviteUserRequestModel} data
* @memberof UmbUserData
*/
invite(data: InviteUserRequestModel): void {
const invitedUser = {
status: UserStateModel.INVITED,
@@ -147,6 +158,27 @@ class UmbUserData extends UmbEntityData<UserResponseModel> {
this.createUser(invitedUser);
}
filter (options: any): PagedUserResponseModel {
const { items: allItems } = this.getAll();
const filterOptions = {
skip: options.skip || 0,
take: options.take || 25,
orderBy: options.orderBy || 'name',
orderDirection: options.orderDirection || 'asc',
userGroupIds: options.userGroupIds,
userStates: options.userStates,
filter: options.filter,
};
const filteredItems = allItems.filter((item) => userGroupFilter(filterOptions, item) && userStateFilter(filterOptions, item) && userQueryFilter(filterOptions, item));
const totalItems = filteredItems.length;
const paginatedItems = filteredItems.slice(filterOptions.skip, filterOptions.skip + filterOptions.take);
return { total: totalItems, items: paginatedItems };
};
}
export const data: Array<UserResponseModel & { type: string }> = [

View File

@@ -87,3 +87,29 @@ export const createFileItemResponseModelBaseModel = (item: any): FileItemRespons
name: item.name,
icon: item.icon,
});
export const arrayFilter = (filterBy: Array<string>, value?: Array<string>): boolean => {
// if a filter is not set, return all items
if (!filterBy) {
return true;
}
return filterBy.some((filterValue: string) => value?.includes(filterValue));
}
export const stringFilter = (filterBy: Array<string>, value?: string): boolean => {
// if a filter is not set, return all items
if (!filterBy || !value) {
return true;
}
return filterBy.includes(value);
};
export const queryFilter = (filterBy: string, value?: string) => {
if (!filterBy || !value) {
return true;
}
const query = filterBy.toLowerCase();
return value.toLowerCase().includes(query);
};

View File

@@ -4,23 +4,16 @@ import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post(umbracoPath(`${slug}`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
const response = umbUsersData.createUser(data);
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}`), (req, res, ctx) => {
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
//TODO: Implementer filter
const response = umbUsersData.getAll();
rest.post(umbracoPath(`${slug}`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
const response = umbUsersData.createUser(data);
return res(ctx.status(200), ctx.json(response));
}),

View File

@@ -0,0 +1,30 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
const skip = Number(req.url.searchParams.get('skip'));
const take = Number(req.url.searchParams.get('take'));
const orderBy = req.url.searchParams.get('orderBy');
const orderDirection = req.url.searchParams.get('orderDirection');
const userGroupIds = req.url.searchParams.getAll('userGroupIds');
const userStates = req.url.searchParams.getAll('userStates');
const filter = req.url.searchParams.get('filter');
const options = {
skip: skip || undefined,
take: take || undefined,
orderBy: orderBy || undefined,
orderDirection: orderDirection || undefined,
userGroupIds: userGroupIds.length > 0 ? userGroupIds : undefined,
userStates: userStates.length > 0 ? userStates : undefined,
filter: filter || undefined,
};
const response = umbUsersData.filter(options);
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -7,6 +7,7 @@ import { handlers as disableHandlers } from './disable.handlers.js';
import { handlers as changePasswordHandlers } from './change-password.handlers.js';
import { handlers as unlockHandlers } from './unlock.handlers.js';
import { handlers as inviteHandlers } from './invite.handlers.js';
import { handlers as filterHandlers } from './filter.handlers.js';
export const handlers = [
...itemHandlers,
@@ -16,6 +17,7 @@ export const handlers = [
...setUserGroupsHandlers,
...changePasswordHandlers,
...unlockHandlers,
...detailHandlers,
...filterHandlers,
...inviteHandlers,
...detailHandlers,
];

View File

@@ -1,3 +1,4 @@
import { UmbCollectionConfiguration } from './types.js';
import { UmbCollectionRepository } from '@umbraco-cms/backoffice/repository';
import { UmbBaseController, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
@@ -10,7 +11,8 @@ import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
import { ManifestCollectionView, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionManager, UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectionFilterModel> extends UmbBaseController {
protected entityType: string;
@@ -19,8 +21,8 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
#items = new UmbArrayState<ItemType>([]);
public readonly items = this.#items.asObservable();
#total = new UmbNumberState(0);
public readonly total = this.#total.asObservable();
#totalItems = new UmbNumberState(0);
public readonly totalItems = this.#totalItems.asObservable();
#selectionManager = new UmbSelectionManager();
public readonly selection = this.#selectionManager.selection;
@@ -37,39 +39,25 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
repository?: UmbCollectionRepository;
collectionRootPathname: string;
constructor(host: UmbControllerHostElement, entityType: string, repositoryAlias: string) {
public readonly pagination = new UmbPaginationManager();
constructor(host: UmbControllerHostElement, entityType: string, repositoryAlias: string, config: UmbCollectionConfiguration = { pageSize: 50 }) {
super(host);
this.entityType = entityType;
this.#selectionManager.setMultiple(true);
// listen for page changes on the pagination manager
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
const currentUrl = new URL(window.location.href);
this.collectionRootPathname = currentUrl.pathname.substring(0, currentUrl.pathname.lastIndexOf('/'));
this.init = Promise.all([
this.observe(
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
if (repositoryManifest) {
const result = await createExtensionApi(repositoryManifest, [this._host]);
this.repository = result as UmbCollectionRepository;
this.requestCollection();
}
},
'umbCollectionRepositoryObserver'
).asPromise(),
this.observe(umbExtensionsRegistry.extensionsOfType('collectionView').pipe(
map((extensions) => {
return extensions.filter((extension) => extension.conditions.entityType === this.getEntityType());
}),
),
(views) => {
this.#views.next(views);
this.#setCurrentView();
}, 'umbCollectionViewsObserver').asPromise(),
this.#observeRepository(repositoryAlias).asPromise(),
this.#observeViews().asPromise(),
]);
this.#configure(config);
this.provideContext(UMB_COLLECTION_CONTEXT, this);
}
@@ -148,8 +136,9 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
const { data } = await this.repository.requestCollection(filter);
if (data) {
this.#total.next(data.total);
this.#items.next(data.items);
this.#totalItems.next(data.total);
this.pagination.setTotalItems(data.total);
}
}
@@ -182,6 +171,44 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
return this.#currentView.getValue();
}
#configure(configuration: UmbCollectionConfiguration) {
this.#selectionManager.setMultiple(true);
this.pagination.setPageSize(configuration.pageSize);
this.#filter.next({ ...this.#filter.getValue(), skip: 0, take: configuration.pageSize });
}
#observeRepository(repositoryAlias: string) {
return this.observe(
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
if (repositoryManifest) {
const result = await createExtensionApi(repositoryManifest, [this._host]);
this.repository = result as UmbCollectionRepository;
this.requestCollection();
}
},
'umbCollectionRepositoryObserver'
)
}
#observeViews() {
return this.observe(umbExtensionsRegistry.extensionsOfType('collectionView').pipe(
map((extensions) => {
return extensions.filter((extension) => extension.conditions.entityType === this.getEntityType());
}),
),
(views) => {
this.#views.next(views);
this.#setCurrentView();
}, 'umbCollectionViewsObserver');
}
#onPageChange = (event: UmbChangeEvent) => {
const target = event.target as UmbPaginationManager;
const skipFilter = { skip: target.getSkip() } as Partial<FilterModelType>;
this.setFilter(skipFilter);
}
#setCurrentView() {
const currentUrl = new URL(window.location.href);
const lastPathSegment = currentUrl.pathname.split('/').pop();

View File

@@ -61,6 +61,7 @@ export class UmbCollectionElement extends UmbLitElement {
<umb-body-layout header-transparent>
${this.renderToolbar()}
<umb-router-slot id="router-slot" .routes="${this._routes}"></umb-router-slot>
${this.renderPagination()}
${this.renderSelectionActions()}
</umb-body-layout>
`;
@@ -70,6 +71,10 @@ export class UmbCollectionElement extends UmbLitElement {
return html`<umb-collection-toolbar slot="header"></umb-collection-toolbar>`;
}
protected renderPagination () {
return html`<umb-collection-pagination></umb-collection-pagination>`;
}
protected renderSelectionActions() {
return html`<umb-collection-selection-actions slot="footer-info"></umb-collection-selection-actions>`;
}

View File

@@ -7,7 +7,7 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-collection-selection-actions')
export class UmbCollectionSelectionActionsElement extends UmbLitElement {
@state()
private _nodesLength = 0;
private _totalItems = 0;
@state()
private _selectionLength = 0;
@@ -40,13 +40,12 @@ export class UmbCollectionSelectionActionsElement extends UmbLitElement {
private _observeCollectionContext() {
if (!this._collectionContext) return;
// TODO: Make sure it only updates on length change.
this.observe(
this._collectionContext.items,
(mediaItems) => {
this._nodesLength = mediaItems.length;
this._collectionContext.totalItems,
(value) => {
this._totalItems = value;
},
'umbItemsLengthObserver',
'umbTotalItemsObserver',
);
this.observe(
@@ -61,7 +60,7 @@ export class UmbCollectionSelectionActionsElement extends UmbLitElement {
}
private _renderSelectionCount() {
return html`<div>${this._selectionLength} of ${this._nodesLength} selected</div>`;
return html`<div>${this._selectionLength} of ${this._totalItems} selected</div>`;
}
#onActionExecuted(event: UmbActionExecutedEvent) {

View File

@@ -1,7 +1,9 @@
import './pagination/collection-pagination.element.js';
import './collection-selection-actions.element.js';
import './collection-toolbar.element.js';
import './collection-view-bundle.element.js';
export * from './pagination/collection-pagination.element.js';
export * from './collection-selection-actions.element.js';
export * from './collection-toolbar.element.js';
export * from './collection-view-bundle.element.js';
export * from './collection-view-bundle.element.js';

View File

@@ -0,0 +1,66 @@
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, nothing, state } from '@umbraco-cms/backoffice/external/lit';
import { UMB_COLLECTION_CONTEXT, UmbCollectionContext } from '@umbraco-cms/backoffice/collection';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-collection-pagination')
export class UmbCollectionPaginationElement extends UmbLitElement {
@state()
_totalPages = 0;
@state()
_currentPage = 1;
private _collectionContext?: UmbCollectionContext<any, any>;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => {
this._collectionContext = instance;
this.#observeCurrentPage();
this.#observerTotalPages();
});
}
#observeCurrentPage () {
this.observe(this._collectionContext!.pagination.currentPage, (currentPage) => {
this._currentPage = currentPage;
}, 'umbCurrentPageObserver');
}
#observerTotalPages () {
this.observe(this._collectionContext!.pagination.totalPages, (totalPages) => {
this._totalPages = totalPages;
}, 'umbTotalPagesObserver');
}
#onChange (event: UUIPaginationEvent) {
this._collectionContext?.pagination.setCurrentPageNumber(event.target.current);
}
render() {
if (this._totalPages <= 1) {
return nothing;
}
return html`<uui-pagination .current=${this._currentPage} .total=${this._totalPages} @change=${this.#onChange}></uui-pagination>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
margin-top: var(--uui-size-layout-1);
}
`
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-collection-pagination': UmbCollectionPaginationElement;
}
}

View File

@@ -0,0 +1,3 @@
export interface UmbCollectionConfiguration {
pageSize: number;
}

View File

@@ -1,6 +1,8 @@
export class UmbChangeEvent extends Event {
public static readonly TYPE = 'change';
public constructor() {
// mimics the native change event
super('change', { bubbles: true, composed: false, cancelable: false });
super(UmbChangeEvent.TYPE, { bubbles: true, composed: false, cancelable: false });
}
}

View File

@@ -1,7 +1,6 @@
import type { UmbPagedData } from '../tree-repository.interface.js';
import type { DataSourceResponse } from '../index.js';
import type { UmbPagedData } from './types.js';
export interface UmbCollectionDataSource<ItemType = any, PagedItemType = UmbPagedData<ItemType>> {
getCollection(): Promise<DataSourceResponse<PagedItemType>>;
filterCollection(filter: any): Promise<DataSourceResponse<PagedItemType>>;
export interface UmbCollectionDataSource<ItemType = any, FilterType = unknown> {
getCollection(filter: FilterType): Promise<DataSourceResponse<UmbPagedData<ItemType>>>;
}

View File

@@ -1,5 +1,5 @@
import { expect } from '@open-wc/testing';
import { type UmbPagedData } from '../tree-repository.interface.js';
import { type UmbPagedData } from './types.js';
import { type DataSourceResponse } from './data-source-response.interface.js';
import { extendDataSourcePagedResponseData } from './extend-data-source-paged-response-data.function.js';

View File

@@ -8,3 +8,4 @@ export * from './folder-data-source.interface.js';
export * from './item-data-source.interface.js';
export * from './move-data-source.interface.js';
export * from './tree-data-source.interface.js';
export * from './types.js';

View File

@@ -1,4 +1,4 @@
import type { UmbPagedData } from '../tree-repository.interface.js';
import type { UmbPagedData } from './types.js';
import type { DataSourceResponse } from './data-source-response.interface.js';
export interface UmbTreeDataSource<ItemType = any, PagedItemType = UmbPagedData<ItemType>> {

View File

@@ -0,0 +1,4 @@
export interface UmbPagedData<T> {
total: number;
items: Array<T>;
}

View File

@@ -1,12 +1,8 @@
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { type UmbPagedData } from './data-source/types.js';
import { type Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbTreeRootEntityModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
import { ProblemDetails, EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export interface UmbPagedData<T> {
total: number;
items: Array<T>;
}
export interface UmbTreeRepository<
TreeItemType extends EntityTreeItemResponseModel,
TreeRootType extends UmbTreeRootModel = UmbTreeRootEntityModel

View File

@@ -27,7 +27,7 @@ export class UmbUserGroupCollectionRepository implements UmbCollectionRepository
async requestCollection(filter: UmbUserGroupCollectionFilterModel = { skip: 0, take: 100 }) {
await this.#init;
const { data, error } = await this.#collectionSource.filterCollection(filter);
const { data, error } = await this.#collectionSource.getCollection(filter);
if (data) {
this.#detailStore?.appendItems(data.items);

View File

@@ -22,11 +22,7 @@ export class UmbUserGroupCollectionServerDataSource implements UmbCollectionData
this.#host = host;
}
getCollection() {
return tryExecuteAndNotify(this.#host, UserGroupResource.getUserGroup({}));
}
filterCollection(filter: UmbUserGroupCollectionFilterModel) {
getCollection(filter: UmbUserGroupCollectionFilterModel) {
// TODO: Switch this to the filter endpoint when available
return tryExecuteAndNotify(this.#host, UserGroupResource.getUserGroup({}));
}

View File

@@ -6,7 +6,7 @@ import { UmbCollectionDataSource, UmbCollectionRepository } from '@umbraco-cms/b
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbUserCollectionRepository extends UmbUserRepositoryBase implements UmbCollectionRepository {
#collectionSource: UmbCollectionDataSource<UserResponseModel>;
#collectionSource: UmbCollectionDataSource<UserResponseModel, UmbUserCollectionFilterModel>;
constructor(host: UmbControllerHost) {
super(host);
@@ -16,7 +16,7 @@ export class UmbUserCollectionRepository extends UmbUserRepositoryBase implement
async requestCollection(filter: UmbUserCollectionFilterModel = { skip: 0, take: 100 }) {
await this.init;
const { data, error } = await this.#collectionSource.filterCollection(filter);
const { data, error } = await this.#collectionSource.getCollection(filter);
if (data) {
this.detailStore!.appendItems(data.items);

View File

@@ -22,25 +22,13 @@ export class UmbUserCollectionServerDataSource implements UmbCollectionDataSourc
this.#host = host;
}
/**
* Gets the user collection from the server.
* @return {*}
* @memberof UmbUserCollectionServerDataSource
*/
async getCollection() {
const response = await tryExecuteAndNotify(this.#host, UserResource.getUser({}));
return extendDataSourcePagedResponseData<UmbUserDetail>(response, {
entityType: USER_ENTITY_TYPE,
});
}
/**
* Gets the user collection filtered by the given filter.
* @param {UmbUserCollectionFilterModel} filter
* @return {*}
* @memberof UmbUserCollectionServerDataSource
*/
async filterCollection(filter: UmbUserCollectionFilterModel) {
async getCollection(filter: UmbUserCollectionFilterModel) {
const response = await tryExecuteAndNotify(this.#host, UserResource.getUserFilter(filter));
return extendDataSourcePagedResponseData<UmbUserDetail>(response, {
entityType: USER_ENTITY_TYPE,

View File

@@ -6,7 +6,7 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api
export class UmbUserCollectionContext extends UmbCollectionContext<UmbUserDetail, UmbUserCollectionFilterModel> {
constructor(host: UmbControllerHostElement) {
super(host, USER_ENTITY_TYPE, USER_COLLECTION_REPOSITORY_ALIAS);
super(host, USER_ENTITY_TYPE, USER_COLLECTION_REPOSITORY_ALIAS, { pageSize: 50 });
}
/**

View File

@@ -8,6 +8,7 @@ export * from './path-folder-name.function.js';
export * from './selection-manager.js';
export * from './udi-service.js';
export * from './umbraco-path.function.js';
export * from './pagination-manager/pagination.manager.js';
declare global {
interface Window {

View File

@@ -0,0 +1,167 @@
import { expect, oneEvent } from '@open-wc/testing';
import { UmbPaginationManager } from './pagination.manager.js';
import { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'
describe('UmbPaginationManager', () => {
let manager: UmbPaginationManager;
beforeEach(() => {
manager = new UmbPaginationManager();
manager.setPageSize(10);
manager.setTotalItems(100);
});
describe('Public API', () => {
describe('properties', () => {
it('has a pageSize property', () => {
expect(manager).to.have.property('pageSize').to.be.an.instanceOf(Observable);
});
it('has a totalItems property', () => {
expect(manager).to.have.property('totalItems').to.be.an.instanceOf(Observable);
});
it('has a currentPage property', () => {
expect(manager).to.have.property('currentPage').to.be.an.instanceOf(Observable);
});
it('has a skip property', () => {
expect(manager).to.have.property('skip').to.be.an.instanceOf(Observable);
});
});
describe('methods', () => {
it('has a setPageSize method', () => {
expect(manager).to.have.property('setPageSize').that.is.a('function');
});
it('has a getPageSize method', () => {
expect(manager).to.have.property('getPageSize').that.is.a('function');
});
it('has a getTotalItems method', () => {
expect(manager).to.have.property('getTotalItems').that.is.a('function');
});
it('has a getTotalPages method', () => {
expect(manager).to.have.property('getTotalPages').that.is.a('function');
});
it('has a getCurrentPageNumber method', () => {
expect(manager).to.have.property('getCurrentPageNumber').that.is.a('function');
});
it('has a setCurrentPageNumber method', () => {
expect(manager).to.have.property('setCurrentPageNumber').that.is.a('function');
});
it('has a getSkip method', () => {
expect(manager).to.have.property('getSkip').that.is.a('function');
});
});
});
describe('Page Size', () => {
it('sets and gets the pageSize value', () => {
manager.setPageSize(5);
expect(manager.getPageSize()).to.equal(5);
});
it('updates the observable', (done) => {
manager.setPageSize(2);
manager.pageSize.subscribe((value) => {
expect(value).to.equal(2);
done();
})
.unsubscribe();
});
});
describe('Total Items', () => {
it('sets and gets the totalItems value', () => {
manager.setTotalItems(200);
expect(manager.getTotalItems()).to.equal(200);
});
it('updates the observable', (done) => {
manager.totalItems.subscribe((value) => {
expect(value).to.equal(100);
done();
})
.unsubscribe();
});
it('recalculates the total pages', () => {
expect(manager.getTotalPages()).to.equal(10);
});
it('it fall backs to the last page number if the totalPages is less than the currentPage', () => {
manager.setCurrentPageNumber(10);
manager.setTotalItems(20);
expect(manager.getCurrentPageNumber()).to.equal(2);
});
});
describe('Current Page', () => {
it('sets and gets the currentPage value', () => {
manager.setCurrentPageNumber(2);
expect(manager.getCurrentPageNumber()).to.equal(2);
});
it ('cant be set to a value less than 1', () => {
manager.setCurrentPageNumber(0);
expect(manager.getCurrentPageNumber()).to.equal(1);
});
it ('cant be set to a value greater than the total pages', () => {
manager.setPageSize(1);
manager.setTotalItems(2);
manager.setCurrentPageNumber(10);
expect(manager.getCurrentPageNumber()).to.equal(2);
});
it('updates the observable', (done) => {
manager.setCurrentPageNumber(2);
manager.currentPage.subscribe((value) => {
expect(value).to.equal(2);
done();
})
.unsubscribe();
});
it('updates the skip value', () => {
manager.setCurrentPageNumber(5);
expect(manager.getSkip()).to.equal(40);
});
it('dispatches a change event', async () => {
const listener = oneEvent(manager, UmbChangeEvent.TYPE);
manager.setCurrentPageNumber(2);
const event = (await listener) as unknown as UmbChangeEvent;
const target = event.target as UmbPaginationManager;
expect(event).to.exist;
expect(event.type).to.equal(UmbChangeEvent.TYPE);
expect(target.getCurrentPageNumber()).to.equal(2);
});
});
describe('Skip', () => {
it('gets the skip value', () => {
manager.setCurrentPageNumber(5);
expect(manager.getSkip()).to.equal(40);
});
it('updates the observable', (done) => {
manager.setCurrentPageNumber(5);
manager.skip.subscribe((value) => {
expect(value).to.equal(40);
done();
})
.unsubscribe();
});
});
});

View File

@@ -0,0 +1,124 @@
import { UmbChangeEvent } from "@umbraco-cms/backoffice/event";
import { UmbNumberState } from "@umbraco-cms/backoffice/observable-api";
export class UmbPaginationManager extends EventTarget {
#pageSize = new UmbNumberState(10);
public readonly pageSize = this.#pageSize.asObservable();
#totalItems = new UmbNumberState(0);
public readonly totalItems = this.#totalItems.asObservable();
#totalPages = new UmbNumberState(0);
public readonly totalPages = this.#totalPages.asObservable();
#currentPage = new UmbNumberState(1);
public readonly currentPage = this.#currentPage.asObservable();
#skip = new UmbNumberState(0);
public readonly skip = this.#skip.asObservable();
/**
* Sets the number of items per page and recalculates the total number of pages
* @param {number} pageSize
* @memberof UmbPaginationManager
*/
public setPageSize(pageSize: number) {
this.#pageSize.next(pageSize);
this.#calculateTotalPages();
}
/**
* Gets the number of items per page
* @return {number}
* @memberof UmbPaginationManager
*/
public getPageSize() {
return this.#pageSize.getValue();
}
/**
* Gets the total number of items
* @return {number}
* @memberof UmbPaginationManager
*/
public getTotalItems() {
return this.#totalItems.getValue();
}
/**
* Sets the total number of items and recalculates the total number of pages
* @param {number} totalItems
* @memberof UmbPaginationManager
*/
public setTotalItems(totalItems: number) {
this.#totalItems.next(totalItems);
this.#calculateTotalPages();
}
/**
* Gets the total number of pages
* @return {number}
* @memberof UmbPaginationManager
*/
public getTotalPages() {
return this.#totalPages.getValue();
}
/**
* Gets the current page number
* @return {number}
* @memberof UmbPaginationManager
*/
public getCurrentPageNumber() {
return this.#currentPage.getValue();
}
/**
* Sets the current page number
* @param {number} pageNumber
* @memberof UmbPaginationManager
*/
public setCurrentPageNumber(pageNumber: number) {
if (pageNumber < 1) {
pageNumber = 1;
}
if (pageNumber > this.#totalPages.getValue()) {
pageNumber = this.#totalPages.getValue();
}
this.#currentPage.next(pageNumber);
this.#calculateSkip();
this.dispatchEvent(new UmbChangeEvent());
}
/**
* Gets the number of items to skip
* @return {number}
* @memberof UmbPaginationManager
*/
public getSkip() {
return this.#skip.getValue();
}
/**
* Calculates the total number of pages
* @memberof UmbPaginationManager
*/
#calculateTotalPages() {
const totalPages = Math.ceil(this.#totalItems.getValue() / this.#pageSize.getValue());
this.#totalPages.next(totalPages);
/* If we currently are on a page higher than the total pages. We need to reset the current page to the last page.
This can happen if we have a filter that returns less items than the current page size. */
if (this.getCurrentPageNumber() > totalPages) {
this.setCurrentPageNumber(totalPages);
}
}
#calculateSkip() {
const skip = (this.#currentPage.getValue() - 1) * this.#pageSize.getValue();
this.#skip.next(skip);
}
}