From 86b4b0d28df05f4bda15272b84d5aa91fd695173 Mon Sep 17 00:00:00 2001 From: Gabriel Guerrero Date: Fri, 5 Apr 2024 10:58:47 +0100 Subject: [PATCH] feat(signals): several fixes - Ensure withEntitiesRemote Filter Sort and Pagination depend on CallStateMethods - Remove comments and console.log - Ensure some imports use type imports - New withEntitiesLocalFilter implementation that doesnt override entities computed, and doesnt have problem with pagination trait been before or after - Fixed bug in withCalls not storing error and signal to read result - withCalls prop is now [callName]Result instead of [callName]Data --- ...list-paginated-page-container.component.ts | 118 ---------- .../product.store.ts | 2 + ...list-paginated-page-container.component.ts | 52 +++-- .../entities-pagination.trait.spec.ts | 217 +++++++++--------- .../lib/with-call-status/with-call-status.ts | 10 - .../signals/src/lib/with-calls/with-calls.ts | 45 +++- .../src/lib/with-calls/with-calls.util.ts | 2 +- .../with-entities-filter.ts | 75 +++--- .../with-entities-loading-call.ts | 2 +- .../with-entities-local-pagination.ts | 37 +-- .../with-entities-local-pagination.util.ts | 34 +++ .../with-entities-multi-selection.ts | 2 +- .../with-entities-remote-pagination.ts | 51 ++-- .../with-entities-single-selection.ts | 2 +- .../with-entities-sort/with-entities-sort.ts | 9 +- 15 files changed, 286 insertions(+), 372 deletions(-) delete mode 100644 apps/example-app/src/app/examples/signals/product-list-paginated-page/product-list-paginated-page-container.component.ts diff --git a/apps/example-app/src/app/examples/signals/product-list-paginated-page/product-list-paginated-page-container.component.ts b/apps/example-app/src/app/examples/signals/product-list-paginated-page/product-list-paginated-page-container.component.ts deleted file mode 100644 index 16fdeaeb..00000000 --- a/apps/example-app/src/app/examples/signals/product-list-paginated-page/product-list-paginated-page-container.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { Component, inject, OnInit } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { Sort } from '@ngrx-traits/common'; - -import { ProductListComponent } from '../../components/product-list/product-list.component'; -import { ProductSearchFormComponent } from '../../components/product-search-form/product-search-form.component'; -import { Product, ProductFilter } from '../../models'; -import { ProductsLocalStore } from './product.store'; - -@Component({ - selector: 'ngrx-traits-product-list-example-container', - template: ` - - - Product List - - - - @if (store.productsLoading()) { - - } @else { - - - - } - - - - - - `, - styles: [ - ` - mat-card-content > mat-spinner { - margin: 10px auto; - } - mat-card-actions mat-spinner { - display: inline-block; - margin-right: 5px; - } - `, - ], - standalone: true, - imports: [ - MatCardModule, - ProductSearchFormComponent, - MatProgressSpinnerModule, - ProductListComponent, - MatPaginatorModule, - MatButtonModule, - AsyncPipe, - ], -}) -export class ProductListPaginatedPageContainerComponent implements OnInit { - store = inject(ProductsLocalStore); - - ngOnInit() { - this.store.loadProductDetail; - this.store.productsFilter; - // this.store.dispatch(ProductActions.loadProductsUsingRouteQueryParams()); - } - - select({ id }: Product) { - this.store.selectProductsEntity({ id }); - } - - checkout() { - this.store.checkout(); - } - - filter(filter: ProductFilter | undefined) { - filter && this.store.filterProductsEntities({ filter }); - } - - sort(sort: Sort) { - this.store.sortProductsEntities({ - sort: { field: sort.active as string, direction: sort.direction }, - }); - } - - loadPage($event: PageEvent) { - this.store.loadProductsPage({ pageIndex: $event.pageIndex }); - } -} diff --git a/apps/example-app/src/app/examples/signals/product-list-paginated-page/product.store.ts b/apps/example-app/src/app/examples/signals/product-list-paginated-page/product.store.ts index 3ce47df0..70deadb7 100644 --- a/apps/example-app/src/app/examples/signals/product-list-paginated-page/product.store.ts +++ b/apps/example-app/src/app/examples/signals/product-list-paginated-page/product.store.ts @@ -121,9 +121,11 @@ export const ProductsLocalStore = signalStore( call: ({ id }: { id: string }) => inject(ProductService).getProductDetail(id), resultProp: 'productDetail', + mapPipe: 'switchMap', }, checkout: () => inject(OrderService).checkout(), })), + withLogger('sdsd'), ); export const ProductsLocalStore2 = signalStore( diff --git a/apps/example-app/src/app/examples/signals/product-list-paginated-page/signal-product-list-paginated-page-container.component.ts b/apps/example-app/src/app/examples/signals/product-list-paginated-page/signal-product-list-paginated-page-container.component.ts index 1e0b1428..ada42f1f 100644 --- a/apps/example-app/src/app/examples/signals/product-list-paginated-page/signal-product-list-paginated-page-container.component.ts +++ b/apps/example-app/src/app/examples/signals/product-list-paginated-page/signal-product-list-paginated-page-container.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe } from '@angular/common'; +import { AsyncPipe, JsonPipe } from '@angular/common'; import { Component, inject, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; @@ -6,6 +6,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { Sort } from '@ngrx-traits/common'; +import { ProductDetailComponent } from '../../components/product-detail/product-detail.component'; import { ProductListComponent } from '../../components/product-list/product-list.component'; import { ProductSearchFormComponent } from '../../components/product-search-form/product-search-form.component'; import { Product, ProductFilter } from '../../models'; @@ -26,23 +27,32 @@ import { ProductsLocalStore } from './product.store'; @if (store.productsLoading()) { } @else { - - - +
+
+ + + +
+ + +
} @@ -83,6 +93,8 @@ import { ProductsLocalStore } from './product.store'; MatPaginatorModule, MatButtonModule, AsyncPipe, + ProductDetailComponent, + JsonPipe, ], }) export class SignalProductListPaginatedPageContainerComponent @@ -91,13 +103,13 @@ export class SignalProductListPaginatedPageContainerComponent store = inject(ProductsLocalStore); ngOnInit() { - this.store.productsFilter; + // this.store.productDetail; this.store.loadProductDetail({ id: '12' }); - // this.store.dispatch(ProductActions.loadProductsUsingRouteQueryParams()); } select({ id }: Product) { this.store.selectProductsEntity({ id }); + this.store.loadProductDetail({ id }); } checkout() { diff --git a/libs/ngrx-traits/common/src/lib/entities-pagination/entities-pagination.trait.spec.ts b/libs/ngrx-traits/common/src/lib/entities-pagination/entities-pagination.trait.spec.ts index 168df276..04534db8 100644 --- a/libs/ngrx-traits/common/src/lib/entities-pagination/entities-pagination.trait.spec.ts +++ b/libs/ngrx-traits/common/src/lib/entities-pagination/entities-pagination.trait.spec.ts @@ -1,31 +1,31 @@ -import { Actions } from '@ngrx/effects'; -import { createAction, createFeatureSelector } from '@ngrx/store'; +import { TestBed } from '@angular/core/testing'; import { createEntityFeatureFactory, FeatureSelectors, } from '@ngrx-traits/core'; -import { addLoadEntitiesTrait } from '../load-entities'; -import { Todo, TodoFilter } from '../load-entities/load-entities.trait.spec'; +import { Actions } from '@ngrx/effects'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Dictionary } from '@ngrx/entity'; +import { createAction, createFeatureSelector } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; +import { first, take, toArray } from 'rxjs/operators'; + +import { addCrudEntitiesTrait, CrudEntitiesState } from '../crud-entities'; import { addFilterEntitiesTrait, FilterEntitiesState, } from '../filter-entities'; -import { TestBed } from '@angular/core/testing'; -import { provideMockActions } from '@ngrx/effects/testing'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { addEntitiesPaginationTrait } from './entities-pagination.trait'; +import { ƟFilterEntitiesActions } from '../filter-entities/filter-entities.model.internal'; import { TestState } from '../filter-entities/filter-entities.trait.spec'; +import { addLoadEntitiesTrait } from '../load-entities'; +import { Todo, TodoFilter } from '../load-entities/load-entities.trait.spec'; import { CacheType, PageInfoModel } from './entities-pagination.model'; -import { of } from 'rxjs'; -import { first, take, toArray } from 'rxjs/operators'; -import { addCrudEntitiesTrait, CrudEntitiesState } from '../crud-entities'; - -import { Dictionary } from '@ngrx/entity'; import { ƟEntitiesPaginationSelectors, ƟPaginationActions, } from './entities-pagination.model.internal'; -import { ƟFilterEntitiesActions } from '../filter-entities/filter-entities.model.internal'; +import { addEntitiesPaginationTrait } from './entities-pagination.trait'; export interface PaginationTestState extends TestState, @@ -43,7 +43,7 @@ describe('Pagination Test', () => { function initWithFilterAndPagination( cacheType: CacheType = 'full', - remoteFilter = true + remoteFilter = true, ) { const featureSelector = createFeatureSelector('test'); const traits = createEntityFeatureFactory( @@ -58,7 +58,7 @@ describe('Pagination Test', () => { ?.toLowerCase() .includes(filter.content.toLowerCase()), }), - addEntitiesPaginationTrait({ cacheType, pageSize: 20 }) + addEntitiesPaginationTrait({ cacheType, pageSize: 20 }), )({ actionsGroupKey: 'test', featureSelector: featureSelector, @@ -80,7 +80,7 @@ describe('Pagination Test', () => { { entityName: 'entity', entitiesName: 'entities' }, addLoadEntitiesTrait(), addCrudEntitiesTrait(), - addEntitiesPaginationTrait({ cacheType, pageSize: 20 }) + addEntitiesPaginationTrait({ cacheType, pageSize: 20 }), )({ actionsGroupKey: 'test', featureSelector: featureSelector, @@ -101,7 +101,7 @@ describe('Pagination Test', () => { currentPage: number, start: number, end: number, - total = todos.length + total = todos.length, ): TestState { return { ...state, @@ -126,7 +126,7 @@ describe('Pagination Test', () => { const state = reducer( initialState, - actions.loadEntitiesPage({ index: 3 }) + actions.loadEntitiesPage({ index: 3 }), ); expect(state).toEqual({ @@ -145,7 +145,7 @@ describe('Pagination Test', () => { const a = actions as unknown as ƟPaginationActions; const state = reducer( initialState, - a.setEntitiesRequestPage({ index: 2 }) + a.setEntitiesRequestPage({ index: 2 }), ); expect(state).toEqual({ @@ -214,7 +214,7 @@ describe('Pagination Test', () => { actions.loadEntitiesSuccess({ entities: todos, total: todos.length, - }) + }), ); // reset(getTimeSpy); expect(state).toEqual(pageState(state, todos, 0, 0, 135)); @@ -231,14 +231,14 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: first3pages, total: todos.length, - }) + }), ); expect(state).toEqual(pageState(state, first3pages, 0, 0, 60)); @@ -249,14 +249,14 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 3, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: next3pages, total: todos.length, - }) + }), ); expect(state).toEqual(pageState(state, next3pages, 3, 60, 120)); @@ -266,14 +266,14 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 6, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: last3pages, total: todos.length, - }) + }), ); expect(state).toEqual(pageState(state, last3pages, 6, 120, 135)); @@ -283,14 +283,14 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 1, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: page2To4, total: todos.length, - }) + }), ); expect(state).toEqual(pageState(state, page2To4, 1, 20, 80)); }); @@ -306,14 +306,14 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: first3pages, total: todos.length, - }) + }), ); expect(state).toEqual(pageState(state, first3pages, 0, 0, 60)); @@ -324,17 +324,17 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 3, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: next3pages, total: todos.length, - }) + }), ); expect(state).toEqual( - pageState(state, [...first3pages, ...next3pages], 3, 0, 120) + pageState(state, [...first3pages, ...next3pages], 3, 0, 120), ); // check last 3 pages @@ -343,14 +343,14 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 6, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: last3pages, total: todos.length, - }) + }), ); expect(state).toEqual( pageState( @@ -358,8 +358,8 @@ describe('Pagination Test', () => { [...first3pages, ...next3pages, ...last3pages], 6, 0, - 135 - ) + 135, + ), ); }); @@ -374,30 +374,30 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: first3pages, total: 60, - }) + }), ); state = reducer( state, actions.loadEntitiesPage({ index: 2, - }) + }), ); state = reducer( state, ( actions as unknown as ƟFilterEntitiesActions - ).storeEntitiesFilter({ filters: { content: '10' } }) + ).storeEntitiesFilter({ filters: { content: '10' } }), ); expect( - selectors.selectEntitiesCurrentPageInfo.projector(state) + selectors.selectEntitiesCurrentPageInfo.projector(state), ).toEqual({ pageIndex: 0, total: 1, @@ -418,34 +418,34 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: first3pages, total: todos.length, - }) + }), ); state = reducer( state, actions.loadEntitiesPage({ index: 2, - }) + }), ); state = reducer( state, ( actions as unknown as ƟFilterEntitiesActions - ).storeEntitiesFilter({ filters: { content: 'something' } }) + ).storeEntitiesFilter({ filters: { content: 'something' } }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: todos.slice(40, 60), total: 20, - }) + }), ); expect(state).toEqual({ ...state, @@ -490,22 +490,22 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: todos, total: todos.length, - }) + }), ); const resultState = reducer( state, actions.addEntities( { id: 123123, content: 'some' }, - { id: 324324, content: 'some2' } - ) + { id: 324324, content: 'some2' }, + ), ); expect(resultState.pagination).toEqual({ @@ -521,14 +521,14 @@ describe('Pagination Test', () => { initialState, actions.loadEntitiesPage({ index: 0, - }) + }), ); state = reducer( state, actions.loadEntitiesSuccess({ entities: todos, total: todos.length, - }) + }), ); const resultState = reducer(state, actions.removeEntities(1, 2)); @@ -549,43 +549,43 @@ describe('Pagination Test', () => { let state = pageState(initialState, todos, 2, 0, 75); // if no page return currentPage expect(selectors.selectEntitiesCurrentPageList.projector(state)).toEqual( - todos.slice(40, 60) + todos.slice(40, 60), ); state = reducer( state, actions.loadEntitiesPage({ index: 0, - }) + }), ); expect(selectors.selectEntitiesCurrentPageList.projector(state)).toEqual( - todos.slice(0, 20) + todos.slice(0, 20), ); state = reducer( state, actions.loadEntitiesPage({ index: 1, - }) + }), ); expect(selectors.selectEntitiesCurrentPageList.projector(state)).toEqual( - todos.slice(20, 40) + todos.slice(20, 40), ); state = reducer( state, actions.loadEntitiesPage({ index: 2, - }) + }), ); expect(selectors.selectEntitiesCurrentPageList.projector(state)).toEqual( - todos.slice(40, 60) + todos.slice(40, 60), ); state = reducer( state, actions.loadEntitiesPage({ index: 3, - }) + }), ); expect(selectors.selectEntitiesCurrentPageList.projector(state)).toEqual( - todos.slice(60, 75) + todos.slice(60, 75), ); }); @@ -604,83 +604,83 @@ describe('Pagination Test', () => { let state = pageState(initialState, todos, 2, 0, 60); // using currentPage expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - true + true, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - false + false, ); state = reducer( state, actions.loadEntitiesPage({ index: 0, - }) + }), ); // using explicit page expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - true + true, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - true + true, ); state = reducer( state, actions.loadEntitiesPage({ index: 1, - }) + }), ); expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - true + true, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - true + true, ); state = reducer( state, actions.loadEntitiesPage({ index: 2, - }) + }), ); expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - true + true, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - false + false, ); state = reducer( state, actions.loadEntitiesPage({ index: 3, - }) + }), ); expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - false + false, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - false + false, ); state = reducer( state, actions.loadEntitiesPage({ index: 4, - }) + }), ); expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - false + false, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - false + false, ); state = reducer( state, actions.loadEntitiesPage({ index: 5, - }) + }), ); expect(selectors.isEntitiesCurrentPageInCache.projector(state)).toEqual( - false + false, ); expect(selectors.isEntitiesNextPageInCache.projector(state)).toEqual( - false + false, ); }); @@ -704,12 +704,12 @@ describe('Pagination Test', () => { state, actions.loadEntitiesPage({ index: 0, - }) + }), ); expect( selectors.selectEntitiesCurrentPage.projector( - pageState(initialState, todos.slice(0, 75), 1, 0, 75, 75) - ) + pageState(initialState, todos.slice(0, 75), 1, 0, 75, 75), + ), ).toEqual({ entities: todos.slice(20, 40), isLoading: false, @@ -727,8 +727,8 @@ describe('Pagination Test', () => { const { selectors, initialState } = initWithFilterAndPagination(); expect( selectors.selectEntitiesCurrentPageInfo.projector( - pageState(initialState, todos.slice(0, 75), 0, 0, 75, 75) - ) + pageState(initialState, todos.slice(0, 75), 0, 0, 75, 75), + ), ).toEqual({ pageIndex: 0, total: 75, @@ -740,8 +740,8 @@ describe('Pagination Test', () => { }); expect( selectors.selectEntitiesCurrentPageInfo.projector( - pageState(initialState, todos.slice(0, 75), 1, 0, 75, 75) - ) + pageState(initialState, todos.slice(0, 75), 1, 0, 75, 75), + ), ).toEqual({ pageIndex: 1, total: 75, @@ -753,8 +753,8 @@ describe('Pagination Test', () => { }); expect( selectors.selectEntitiesCurrentPageInfo.projector( - pageState(initialState, todos.slice(0, 75), 2, 0, 75, 75) - ) + pageState(initialState, todos.slice(0, 75), 2, 0, 75, 75), + ), ).toEqual({ pageIndex: 2, total: 75, @@ -766,8 +766,8 @@ describe('Pagination Test', () => { }); expect( selectors.selectEntitiesCurrentPageInfo.projector( - pageState(initialState, todos.slice(0, 75), 3, 0, 75, 75) - ) + pageState(initialState, todos.slice(0, 75), 3, 0, 75, 75), + ), ).toEqual({ pageIndex: 3, total: 75, @@ -782,7 +782,7 @@ describe('Pagination Test', () => { it('selectEntitiesPagedRequest ', () => { const { selectors, initialState } = initWithFilterAndPagination(); expect( - selectors.selectEntitiesPagedRequest.projector(initialState) + selectors.selectEntitiesPagedRequest.projector(initialState), ).toEqual({ page: 0, size: 60, @@ -795,13 +795,13 @@ describe('Pagination Test', () => { 3, 0, 75, - 75 + 75, ); expect( selectors.selectEntitiesPagedRequest.projector({ ...testState, pagination: { ...testState.pagination, requestPage: 3 }, - }) + }), ).toEqual({ page: 3, size: 60, @@ -818,14 +818,14 @@ describe('Pagination Test', () => { 2, 0, 75, - 75 + 75, ); expect( selectors.isLoadingEntitiesCurrentPage.projector({ ...testState, status: 'loading', pagination: { ...testState.pagination, requestPage: 3 }, - }) + }), ).toEqual(false); }); @@ -838,14 +838,14 @@ describe('Pagination Test', () => { 3, 0, 75, - 75 + 75, ); expect( selectors.isLoadingEntitiesCurrentPage.projector({ ...testState, status: 'loading', pagination: { ...testState.pagination, requestPage: 3 }, - }) + }), ).toEqual(true); }); @@ -858,14 +858,14 @@ describe('Pagination Test', () => { 3, 0, 75, - 75 + 75, ); expect( selectors.isLoadingEntitiesCurrentPage.projector({ ...testState, status: 'success', pagination: { ...testState.pagination, requestPage: 3 }, - }) + }), ).toEqual(false); }); }); @@ -976,7 +976,7 @@ describe('Pagination Test', () => { actions$ = of(actions.loadEntitiesPage({ index: 1 })); mockStore.overrideSelector( selectors.isEntitiesCurrentPageInCache, - true + true, ); const action = await effects.loadPage$.pipe(first()).toPromise(); expect(action).toEqual(actions.loadEntitiesPageSuccess()); @@ -996,7 +996,7 @@ describe('Pagination Test', () => { actions$ = of(actions.loadEntitiesPage({ index: 1 })); mockStore.overrideSelector( selectors.isEntitiesCurrentPageInCache, - false + false, ); const action = await effects.loadPage$.pipe(first()).toPromise(); expect(action).toEqual(actions.loadEntities()); @@ -1016,7 +1016,7 @@ describe('Pagination Test', () => { actions$ = of(actions.loadEntitiesPage({ index: 1, forceLoad: true })); mockStore.overrideSelector( selectors.isEntitiesCurrentPageInCache, - true + true, ); const action = await effects.loadPage$.pipe(first()).toPromise(); expect(action).toEqual(actions.loadEntities()); @@ -1028,7 +1028,7 @@ describe('Pagination Test', () => { cacheType: CacheType, total: number | null = 10 * 20, hasNext = true, - isEntitiesPageInCache = false + isEntitiesPageInCache = false, ) { const { effects, @@ -1040,11 +1040,10 @@ describe('Pagination Test', () => { TestState, ƟEntitiesPaginationSelectors >; - console.log(s); actions$ = of(actions.loadEntitiesPageSuccess()); mockStore.overrideSelector( selectors.isEntitiesNextPageInCache, - isEntitiesPageInCache + isEntitiesPageInCache, ); mockStore.overrideSelector(selectors.selectEntitiesCurrentPageInfo, { hasNext, diff --git a/libs/ngrx-traits/signals/src/lib/with-call-status/with-call-status.ts b/libs/ngrx-traits/signals/src/lib/with-call-status/with-call-status.ts index eec58ee5..9156806b 100644 --- a/libs/ngrx-traits/signals/src/lib/with-call-status/with-call-status.ts +++ b/libs/ngrx-traits/signals/src/lib/with-call-status/with-call-status.ts @@ -47,14 +47,6 @@ export type NamedCallStateMethods = { [K in Prop as `set${Capitalize}Error`]: () => void; }; -export function setLoading(): { callState: 'loading' } { - return { callState: 'loading' }; -} - -export function setLoaded(): { callState: 'loaded' } { - return { callState: 'loaded' }; -} - export function withCallStatus(config?: { initialValue?: CallStatus; }): SignalStoreFeature< @@ -100,12 +92,10 @@ export function withCallStatus({ return { [loadingKey]: computed(() => { - console.log('callState()', callState()); return callState() === 'loading'; }), [loadedKey]: computed(() => callState() === 'loaded'), [errorKey]: computed(() => { - // TODO should we handle storing the error const v = callState(); return typeof v === 'object' ? v.error : null; }), diff --git a/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts b/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts index 24b83c74..03c12a4a 100644 --- a/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts +++ b/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts @@ -14,13 +14,14 @@ import { withState, } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { +import type { SignalStoreFeatureResult, SignalStoreSlices, } from '@ngrx/signals/src/signal-store-models'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { catchError, + concatMap, exhaustMap, first, from, @@ -28,13 +29,13 @@ import { Observable, of, pipe, + switchMap, } from 'rxjs'; import { CallStatus, NamedCallState, NamedCallStateComputed, - NamedCallStateMethods, } from '../with-call-status/with-call-status'; import { getWithCallStatusKeys } from '../with-call-status/with-call-status.util'; import { getWithCallKeys } from './with-calls.util'; @@ -48,7 +49,8 @@ type CallConfig< PropName extends string = string, > = { call: Call; - resultProp: PropName; + resultProp?: PropName; + mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap'; }; export type ExtractCallResultType = @@ -74,8 +76,10 @@ export function withCalls< Input & { state: NamedCallState & { [K in keyof Calls as Calls[K] extends CallConfig - ? Calls[K]['resultProp'] & string - : `${K & string}Data`]?: ExtractCallResultType; + ? Calls[K]['resultProp'] extends string + ? Calls[K]['resultProp'] + : `${K & string}Result` + : `${K & string}Result`]?: ExtractCallResultType; }; signals: NamedCallStateComputed; methods: { @@ -91,10 +95,17 @@ export function withCalls< } as SignalStoreSlices & Input['signals'] & Input['methods']); - const callsState = Object.keys(calls).reduce( - (acc, callName) => { + const callsState = Object.entries(calls).reduce( + (acc, [callName, call]) => { const { callStatusKey } = getWithCallStatusKeys({ prop: callName }); acc[callStatusKey] = 'init'; + const { resultPropKey } = getWithCallKeys({ + callName, + resultProp: isCallConfig(call) + ? call.resultProp + : `${callName}Result`, + }); + acc[resultPropKey] = undefined; return acc; }, {} as Record, @@ -135,8 +146,13 @@ export function withCalls< callName, resultProp: isCallConfig(call) ? call.resultProp - : `${callName}Data`, + : `${callName}Result`, }); + + const mapPipe = + isCallConfig(call) && call.mapPipe + ? mapPipes[call.mapPipe] + : exhaustMap; const setLoading = () => patchState(store, { [callStatusKey]: 'loading', @@ -157,13 +173,13 @@ export function withCalls< patchState(store, { [callStatusKey]: 'loaded', } as StateSignal); - acc[setErrorKey] = () => + acc[setErrorKey] = (error?: unknown) => patchState(store, { - [callStatusKey]: 'fail', + [callStatusKey]: { error }, } as StateSignal); acc[callNameKey] = rxMethod( pipe( - exhaustMap((params) => { + mapPipe((params) => { setLoading(); return runInInjectionContext(environmentInjector, () => { return from( @@ -199,3 +215,8 @@ export function withCalls< function isCallConfig(call: Call | CallConfig): call is CallConfig { return typeof call === 'object'; } +const mapPipes = { + switchMap: switchMap, + concatMap: concatMap, + exhaustMap: exhaustMap, +}; diff --git a/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.util.ts b/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.util.ts index 5c7a93b3..d4030e04 100644 --- a/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.util.ts +++ b/libs/ngrx-traits/signals/src/lib/with-calls/with-calls.util.ts @@ -1,6 +1,6 @@ export function getWithCallKeys({ callName, - resultProp = `${callName}Data`, + resultProp = `${callName}Result`, }: { callName: string; resultProp?: string; diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-filter/with-entities-filter.ts b/libs/ngrx-traits/signals/src/lib/with-entities-filter/with-entities-filter.ts index 3b412f43..66fbd090 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-filter/with-entities-filter.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-filter/with-entities-filter.ts @@ -3,17 +3,17 @@ import { patchState, signalStoreFeature, SignalStoreFeature, - withComputed, withMethods, withState, } from '@ngrx/signals'; import { EntityState, NamedEntityState } from '@ngrx/signals/entities'; -import type { +import { + EntityMap, EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { concatMap, debounce, @@ -111,29 +111,18 @@ export function withEntitiesLocalFilter< entity?: Entity; collection?: Collection; }): SignalStoreFeature { - // TODO throw error if pagination trait is present before this one or find a way to make it not matter - const { entitiesKey } = getWithEntitiesKeys(config); + const { entityMapKey, idsKey } = getWithEntitiesKeys(config); const { filterEntitiesKey, filterKey } = getWithEntitiesFilterKeys(config); return signalStoreFeature( withState({ [filterKey]: defaultFilter }), - withComputed((state: Record>) => { - const entities = state[entitiesKey] as Signal; - const filter = state[filterKey] as Signal; - return { - // TODO: there is a problem with this implementation - // I dont like to much it overrides the entities computed, which could cause a bug if someone - // puts this trait after pagination, I possible fix is to make the local filter set the id of the filtered entities - // the original ids array of the state via the pathState method, and cache the previous ids in another prop so they - // can be used to restore the original entities when the filter changes or is cleared - [entitiesKey]: computed(() => { - return entities().filter((entity) => { - return filterFn(entity, filter()); - }); - }), - }; - }), withMethods((state: Record>) => { const filter = state[filterKey] as Signal; + const entitiesMap = state[entityMapKey] as Signal>; + // we create a computed entities that relies on the entitiesMap instead of + // using the computed state.entities from the withEntities , because this local filter is going to replace + // the ids array of the state with the filtered ids array, and the state.entities depends on it, + // so hour filter function needs the full list of entities always which will be always so we get them from entityMap + const entities = computed(() => Object.values(entitiesMap())); return { [filterEntitiesKey]: rxMethod<{ filter: Filter; @@ -141,10 +130,22 @@ export function withEntitiesLocalFilter< patch?: boolean; forceLoad?: boolean; }>( - debounceFilterPipe( - filter, - filterKey, - state as StateSignal>, + pipe( + debounceFilterPipe(filter), + tap((value) => { + const newEntities = entities().filter((entity) => { + return filterFn(entity, value.filter); + }); + patchState( + state as StateSignal>, + { + [filterKey]: value.filter, + }, + { + [idsKey]: newEntities.map((entity) => entity.id), + }, + ); + }), ), ), }; @@ -218,12 +219,13 @@ export function withEntitiesRemoteFilter< forceLoad?: boolean; }>( pipe( - debounceFilterPipe( - filter, - filterKey, - state as StateSignal>, - ), - tap(() => setLoading()), + debounceFilterPipe(filter), + tap((value) => { + setLoading(); + patchState(state as StateSignal>, { + [filterKey]: value.filter, + }); + }), ), ), }; @@ -231,11 +233,7 @@ export function withEntitiesRemoteFilter< ); } -function debounceFilterPipe( - filter: Signal, - filterKey: string, - store: StateSignal>, -) { +function debounceFilterPipe(filter: Signal) { return pipe( debounce( (value: { @@ -249,7 +247,7 @@ function debounceFilterPipe( payload.patch ? of({ ...payload, - filter: { ...filter?.() }, + filter: { ...filter?.(), ...payload?.filter }, }) : of(payload), ), @@ -258,8 +256,5 @@ function debounceFilterPipe( !current?.forceLoad && JSON.stringify(previous?.filter) === JSON.stringify(current?.filter), ), - tap((value) => { - patchState(store, { [filterKey]: value.filter }); - }), ); } diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-loading-call/with-entities-loading-call.ts b/libs/ngrx-traits/signals/src/lib/with-entities-loading-call/with-entities-loading-call.ts index fe2275f3..c69fef35 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-loading-call/with-entities-loading-call.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-loading-call/with-entities-loading-call.ts @@ -20,7 +20,7 @@ import { EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; -import { +import type { EmptyFeatureResult, SignalStoreFeatureResult, SignalStoreSlices, diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.ts b/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.ts index bef0f903..f25485af 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.ts @@ -17,7 +17,10 @@ import { import { getWithEntitiesKeys } from '../util'; import { getWithEntitiesFilterKeys } from '../with-entities-filter/with-entities-filter.util'; import { getWithEntitiesSortKeys } from '../with-entities-sort/with-entities-sort.util'; -import { getWithEntitiesLocalPaginationKeys } from './with-entities-local-pagination.util'; +import { + getWithEntitiesLocalPaginationKeys, + gotoFirstPageIfFilterOrSortChanges, +} from './with-entities-local-pagination.util'; export type EntitiesPaginationLocalState = { pagination: { @@ -119,7 +122,6 @@ export function withEntitiesLocalPagination< entity?: Entity; collection?: Collection; } = {}): SignalStoreFeature { - // TODO fix the any type here const { entitiesKey } = getWithEntitiesKeys(config); const { filterKey } = getWithEntitiesFilterKeys(config); const { sortKey } = getWithEntitiesSortKeys(config); @@ -186,30 +188,13 @@ export function withEntitiesLocalPagination< }), withHooks({ onInit: (input) => { - // we need reset the currentPage to 0 when the filter or sorting changes - if (filterKey in input || sortKey in input) { - const filter = input[filterKey] as Signal; - const sort = input[sortKey] as Signal; - const entitiesCurrentPage = input[ - entitiesCurrentPageKey - ] as Signal; - const loadEntitiesPage = input[loadEntitiesPageKey] as (options: { - pageIndex: number; - }) => void; - effect( - () => { - // we need to call filter or sort signals if available so - // this effect gets call when they change - filter?.(); - sort?.(); - if (entitiesCurrentPage().pageIndex > 0) { - console.log('local filter or sort changed, reseting page to 0'); - loadEntitiesPage({ pageIndex: 0 }); - } - }, - { allowSignalWrites: true }, - ); - } + gotoFirstPageIfFilterOrSortChanges( + input, + filterKey, + sortKey, + entitiesCurrentPageKey, + loadEntitiesPageKey, + ); }, }), ); diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.util.ts b/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.util.ts index 4f6571c2..37c86159 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.util.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-local-pagination/with-entities-local-pagination.util.ts @@ -1,3 +1,5 @@ +import { effect, Signal } from '@angular/core'; + import { capitalize } from '../util'; export function getWithEntitiesLocalPaginationKeys(config?: { @@ -15,3 +17,35 @@ export function getWithEntitiesLocalPaginationKeys(config?: { : 'loadEntitiesPage', }; } + +export function gotoFirstPageIfFilterOrSortChanges( + input: Record, + filterKey: string, + sortKey: string, + entitiesCurrentPageKey: string, + loadEntitiesPageKey: string, +) { + if (filterKey in input || sortKey in input) { + const filter = input[filterKey] as Signal; + const sort = input[sortKey] as Signal; + const entitiesCurrentPage = input[entitiesCurrentPageKey] as Signal; + const loadEntitiesPage = input[loadEntitiesPageKey] as (options: { + pageIndex: number; + }) => void; + let lastFilter = filter?.(); + let lastSort = sort?.(); + effect( + () => { + if ( + entitiesCurrentPage().pageIndex > 0 && + (lastFilter !== filter?.() || lastSort !== sort?.()) + ) { + lastFilter = filter?.(); + lastSort = sort?.(); + loadEntitiesPage({ pageIndex: 0 }); + } + }, + { allowSignalWrites: true }, + ); + } +} diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-multi-selection/with-entities-multi-selection.ts b/libs/ngrx-traits/signals/src/lib/with-entities-multi-selection/with-entities-multi-selection.ts index b273e443..8679ffd6 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-multi-selection/with-entities-multi-selection.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-multi-selection/with-entities-multi-selection.ts @@ -15,7 +15,7 @@ import { EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { capitalize, getWithEntitiesKeys } from '../util'; import { getWithEntitiesFilterKeys } from '../with-entities-filter/with-entities-filter.util'; diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-remote-pagination/with-entities-remote-pagination.ts b/libs/ngrx-traits/signals/src/lib/with-entities-remote-pagination/with-entities-remote-pagination.ts index ba522ab9..3518fbc1 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-remote-pagination/with-entities-remote-pagination.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-remote-pagination/with-entities-remote-pagination.ts @@ -13,21 +13,28 @@ import { NamedEntityState, setAllEntities, } from '@ngrx/signals/entities'; -import { +import type { EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { pipe, tap } from 'rxjs'; import { getWithEntitiesKeys } from '../util'; +import { + CallStateComputed, + CallStateMethods, + NamedCallStateComputed, + NamedCallStateMethods, +} from '../with-call-status/with-call-status'; import { getWithCallStatusKeys } from '../with-call-status/with-call-status.util'; import { getWithEntitiesFilterKeys } from '../with-entities-filter/with-entities-filter.util'; import type { EntitiesPaginationLocalMethods, NamedEntitiesPaginationLocalMethods, } from '../with-entities-local-pagination/with-entities-local-pagination'; +import { gotoFirstPageIfFilterOrSortChanges } from '../with-entities-local-pagination/with-entities-local-pagination.util'; import { getWithEntitiesSortKeys } from '../with-entities-sort/with-entities-sort.util'; import { getWithEntitiesRemotePaginationKeys } from './with-entities-remote-pagination.util'; @@ -123,8 +130,8 @@ export function withEntitiesRemotePagination< }): SignalStoreFeature< { state: EntityState; - signals: EntitySignals; - methods: {}; + signals: EntitySignals & CallStateComputed; + methods: CallStateMethods; }, { state: EntitiesPaginationRemoteState; @@ -144,8 +151,9 @@ export function withEntitiesRemotePagination< }): SignalStoreFeature< { state: NamedEntityState; // if put Collection the some props get lost and can only be access ['prop'] weird bug - signals: NamedEntitySignals; - methods: {}; + signals: NamedEntitySignals & + NamedCallStateComputed; + methods: NamedCallStateMethods; }, { state: NamedEntitiesPaginationRemoteState; @@ -318,30 +326,13 @@ export function withEntitiesRemotePagination< }), withHooks({ onInit: (input) => { - // we need reset the currentPage to 0 when the filter or sorting changes - if (filterKey in input || sortKey in input) { - const filter = input[filterKey] as Signal; - const sort = input[sortKey] as Signal; - const entitiesCurrentPage = input[ - entitiesCurrentPageKey - ] as Signal; - const loadEntitiesPage = input[ - loadEntitiesPageKey - ] as EntitiesPaginationRemoteMethods['loadEntitiesPage']; - effect( - () => { - // we need to call filter or sort signals if available so - // this effect gets call when they change - filter?.(); - sort?.(); - if (entitiesCurrentPage().pageIndex > 0) { - console.log('local filter or sort changed, reseting page to 0'); - loadEntitiesPage({ pageIndex: 0 }); - } - }, - { allowSignalWrites: true }, - ); - } + gotoFirstPageIfFilterOrSortChanges( + input, + filterKey, + sortKey, + entitiesCurrentPageKey, + loadEntitiesPageKey, + ); }, }), ); diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-single-selection/with-entities-single-selection.ts b/libs/ngrx-traits/signals/src/lib/with-entities-single-selection/with-entities-single-selection.ts index c79c64a6..fc0af51c 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-single-selection/with-entities-single-selection.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-single-selection/with-entities-single-selection.ts @@ -13,7 +13,7 @@ import { EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { capitalize, getWithEntitiesKeys } from '../util'; diff --git a/libs/ngrx-traits/signals/src/lib/with-entities-sort/with-entities-sort.ts b/libs/ngrx-traits/signals/src/lib/with-entities-sort/with-entities-sort.ts index 1cc93ccc..ebc35f14 100644 --- a/libs/ngrx-traits/signals/src/lib/with-entities-sort/with-entities-sort.ts +++ b/libs/ngrx-traits/signals/src/lib/with-entities-sort/with-entities-sort.ts @@ -15,10 +15,13 @@ import { EntitySignals, NamedEntitySignals, } from '@ngrx/signals/entities/src/models'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; +import type { StateSignal } from '@ngrx/signals/src/state-signal'; import { getWithEntitiesKeys } from '../util'; -import { NamedCallStateMethods } from '../with-call-status/with-call-status'; +import { + CallStateMethods, + NamedCallStateMethods, +} from '../with-call-status/with-call-status'; import { getWithCallStatusKeys } from '../with-call-status/with-call-status.util'; import { getWithEntitiesSortKeys } from './with-entities-sort.util'; import { Sort, sortData, SortDirection } from './with-entities-sort.utils'; @@ -127,7 +130,7 @@ export function withEntitiesRemoteSort< { state: EntityState; signals: EntitySignals; - methods: {}; + methods: CallStateMethods; }, { state: EntitiesSortState;