diff --git a/src/app/extensions/quoting/facades/quoting.facade.ts b/src/app/extensions/quoting/facades/quoting.facade.ts index 93a36a29e0..16982dac0b 100644 --- a/src/app/extensions/quoting/facades/quoting.facade.ts +++ b/src/app/extensions/quoting/facades/quoting.facade.ts @@ -16,7 +16,7 @@ import { getCurrentQuotes, getQuoteError, getQuoteLoading, - getSelectedQuote, + getSelectedQuoteWithProducts, } from '../store/quote'; import { AddBasketToQuoteRequest, @@ -30,11 +30,11 @@ import { UpdateQuoteRequest, UpdateQuoteRequestItems, UpdateSubmitQuoteRequest, - getActiveQuoteRequest, + getActiveQuoteRequestWithProducts, getCurrentQuoteRequests, getQuoteRequestError, getQuoteRequestLoading, - getSelectedQuoteRequest, + getSelectedQuoteRequestWithProducts, } from '../store/quote-request'; const getQuotesAndQuoteRequests = createSelector( @@ -49,7 +49,7 @@ export class QuotingFacade { constructor(private store: Store<{}>) {} // QUOTE - quote$ = this.store.pipe(select(getSelectedQuote)); + quote$ = this.store.pipe(select(getSelectedQuoteWithProducts)); quoteLoading$ = this.store.pipe(select(getQuoteLoading)); quoteError$ = this.store.pipe(select(getQuoteError)); @@ -75,10 +75,10 @@ export class QuotingFacade { } // QUOTE REQUEST - quoteRequest$ = this.store.pipe(select(getSelectedQuoteRequest)); + quoteRequest$ = this.store.pipe(select(getSelectedQuoteRequestWithProducts)); quoteRequestLoading$ = this.store.pipe(select(getQuoteRequestLoading)); quoteRequestError$ = this.store.pipe(select(getQuoteRequestError)); - activeQuoteRequest$ = this.store.pipe(select(getActiveQuoteRequest)); + activeQuoteRequest$ = this.store.pipe(select(getActiveQuoteRequestWithProducts)); quoteRequests$() { this.loadQuoteRequests(); diff --git a/src/app/extensions/quoting/services/quote-request/quote-request.service.ts b/src/app/extensions/quoting/services/quote-request/quote-request.service.ts index ec8c88f45c..003ce0d962 100644 --- a/src/app/extensions/quoting/services/quote-request/quote-request.service.ts +++ b/src/app/extensions/quoting/services/quote-request/quote-request.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable, combineLatest, of, throwError } from 'rxjs'; -import { concatMap, filter, map, mapTo, shareReplay, take } from 'rxjs/operators'; +import { concatMap, map, mapTo, shareReplay, take } from 'rxjs/operators'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; import { Link } from 'ish-core/models/link/link.model'; import { ApiService, resolveLinks, unpackEnvelope } from 'ish-core/services/api/api.service'; import { getLoggedInCustomer, getLoggedInUser } from 'ish-core/store/user'; +import { whenFalsy } from 'ish-core/utils/operators'; import { QuoteLineItemResult } from '../../models/quote-line-item-result/quote-line-item-result.model'; import { QuoteRequestItemData } from '../../models/quote-request-item/quote-request-item.interface'; @@ -14,7 +15,7 @@ import { QuoteRequestItemMapper } from '../../models/quote-request-item/quote-re import { QuoteRequestItem } from '../../models/quote-request-item/quote-request-item.model'; import { QuoteRequestData } from '../../models/quote-request/quote-request.interface'; import { QuoteRequest } from '../../models/quote-request/quote-request.model'; -import { getActiveQuoteRequest } from '../../store/quote-request'; +import { getActiveQuoteRequestWithProducts } from '../../store/quote-request'; /** * The Quote Request Service handles the interaction with the 'quoteRequest' related REST API. @@ -46,8 +47,8 @@ export class QuoteRequestService { // rebuild the stream everytime the selected id switches back to undefined store .pipe( - select(getActiveQuoteRequest), - filter(x => !x) + select(getActiveQuoteRequestWithProducts), + whenFalsy() ) .subscribe(() => this.buildActiveQuoteRequestStream()); @@ -252,7 +253,7 @@ export class QuoteRequestService { * selects or creates editable quote request */ private buildActiveQuoteRequestStream() { - this.quoteRequest$ = this.store.pipe(select(getActiveQuoteRequest)).pipe( + this.quoteRequest$ = this.store.pipe(select(getActiveQuoteRequestWithProducts)).pipe( take(1), concatMap(quoteRequest => (quoteRequest ? of(quoteRequest.id) : this.addQuoteRequest())), shareReplay(1) diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts b/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts index 4855d383f8..1213c1dec1 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts @@ -35,7 +35,11 @@ import { QuoteRequestService } from '../../services/quote-request/quote-request. import { QuoteActionTypes } from '../quote/quote.actions'; import * as actions from './quote-request.actions'; -import { getCurrentQuoteRequests, getSelectedQuoteRequest, getSelectedQuoteRequestId } from './quote-request.selectors'; +import { + getCurrentQuoteRequests, + getSelectedQuoteRequestId, + getSelectedQuoteRequestWithProducts, +} from './quote-request.selectors'; @Injectable() export class QuoteRequestEffects { @@ -149,7 +153,7 @@ export class QuoteRequestEffects { @Effect() createQuoteRequestFromQuoteRequest$ = this.actions$.pipe( ofType(actions.QuoteRequestActionTypes.CreateQuoteRequestFromQuoteRequest), - withLatestFrom(this.store.pipe(select(getSelectedQuoteRequest))), + withLatestFrom(this.store.pipe(select(getSelectedQuoteRequestWithProducts))), concatMap(([, currentQuoteRequest]) => this.quoteRequestService.createQuoteRequestFromQuoteRequest(currentQuoteRequest).pipe( map(quoteLineItemResult => new actions.CreateQuoteRequestFromQuoteRequestSuccess({ quoteLineItemResult })), @@ -250,7 +254,7 @@ export class QuoteRequestEffects { updateQuoteRequestItems$ = this.actions$.pipe( ofType(actions.QuoteRequestActionTypes.UpdateQuoteRequestItems), mapToPayloadProperty('lineItemUpdates'), - withLatestFrom(this.store.pipe(select(getSelectedQuoteRequest))), + withLatestFrom(this.store.pipe(select(getSelectedQuoteRequestWithProducts))), map(([lineItemUpdates, selectedQuoteRequest]) => ({ quoteRequestId: selectedQuoteRequest.id, updatedItems: this.filterQuoteRequestsForChanges(lineItemUpdates, selectedQuoteRequest), diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.reducer.spec.ts b/src/app/extensions/quoting/store/quote-request/quote-request.reducer.spec.ts index e9eabd5922..ab162a2460 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.reducer.spec.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.reducer.spec.ts @@ -49,7 +49,8 @@ describe('Quote Request Reducer', () => { const action = new fromActions.LoadQuoteRequestsSuccess({ quoteRequests }); const state = quoteRequestReducer(initialState, action); - expect(state.quoteRequests).toEqual(quoteRequests); + expect(state.ids).toEqual(['test']); + expect(state.entities).toEqual({ test: quoteRequests[0] }); expect(state.loading).toBeFalse(); }); }); diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.reducer.ts b/src/app/extensions/quoting/store/quote-request/quote-request.reducer.ts index 8913d64b06..00fda0fa66 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.reducer.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.reducer.ts @@ -1,3 +1,5 @@ +import { EntityState, createEntityAdapter } from '@ngrx/entity'; + import { HttpError } from 'ish-core/models/http-error/http-error.model'; import { UserAction, UserActionTypes } from 'ish-core/store/user'; @@ -6,21 +8,21 @@ import { QuoteRequestData } from '../../models/quote-request/quote-request.inter import { QuoteAction, QuoteRequestActionTypes } from './quote-request.actions'; -export interface QuoteRequestState { - quoteRequests: QuoteRequestData[]; +export const quoteRequestAdapter = createEntityAdapter(); + +export interface QuoteRequestState extends EntityState { quoteRequestItems: QuoteRequestItem[]; loading: boolean; error: HttpError; selected: string; } -export const initialState: QuoteRequestState = { - quoteRequests: [], +export const initialState: QuoteRequestState = quoteRequestAdapter.getInitialState({ quoteRequestItems: [], loading: false, error: undefined, selected: undefined, -}; +}); export function quoteRequestReducer(state = initialState, action: QuoteAction | UserAction): QuoteRequestState { switch (action.type) { @@ -76,9 +78,12 @@ export function quoteRequestReducer(state = initialState, action: QuoteAction | case QuoteRequestActionTypes.LoadQuoteRequestsSuccess: { const quoteRequests = action.payload.quoteRequests; + if (!state) { + return; + } + return { - ...state, - quoteRequests, + ...quoteRequestAdapter.addAll(quoteRequests, state), loading: false, }; } diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.selectors.spec.ts b/src/app/extensions/quoting/store/quote-request/quote-request.selectors.spec.ts index cd74d212ee..9dc9dd24d5 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.selectors.spec.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.selectors.spec.ts @@ -22,12 +22,13 @@ import { } from './quote-request.actions'; import { getActiveQuoteRequest, + getActiveQuoteRequestWithProducts, getCurrentQuoteRequests, getQuoteRequestError, + getQuoteRequestItemsWithProducts, getQuoteRequestLoading, - getQuoteRequstItems, - getSelectedQuoteRequest, getSelectedQuoteRequestId, + getSelectedQuoteRequestWithProducts, } from './quote-request.selectors'; describe('Quote Request Selectors', () => { @@ -79,7 +80,7 @@ describe('Quote Request Selectors', () => { }; expect(getSelectedQuoteRequestId(store$.state)).toEqual('test'); - expect(getSelectedQuoteRequest(store$.state)).toEqual(expected); + expect(getSelectedQuoteRequestWithProducts(store$.state)).toEqual(expected); }); }); @@ -124,14 +125,14 @@ describe('Quote Request Selectors', () => { store$.dispatch(new LoadQuoteRequestItemsSuccess({ quoteRequestItems })); expect(getQuoteRequestLoading(store$.state)).toBeFalse(); - expect(getQuoteRequstItems(store$.state)).toEqual(quoteRequestItems); + expect(getQuoteRequestItemsWithProducts(store$.state)).toEqual(quoteRequestItems); expect(getActiveQuoteRequest(store$.state)).toBeUndefined(); }); it('should set loading to false and set error state', () => { store$.dispatch(new LoadQuoteRequestItemsFail({ error: { message: 'invalid' } as HttpError })); expect(getQuoteRequestLoading(store$.state)).toBeFalse(); - expect(getQuoteRequstItems(store$.state)).toBeEmpty(); + expect(getQuoteRequestItemsWithProducts(store$.state)).toBeEmpty(); expect(getQuoteRequestError(store$.state)).toEqual({ message: 'invalid' }); }); }); @@ -149,7 +150,7 @@ describe('Quote Request Selectors', () => { }); it('should have a product on the active quote request', () => { - const activeQuoteRequest = getActiveQuoteRequest(store$.state); + const activeQuoteRequest = getActiveQuoteRequestWithProducts(store$.state); expect(activeQuoteRequest).toBeTruthy(); const items = activeQuoteRequest.items; expect(items).toHaveLength(1); diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.selectors.ts b/src/app/extensions/quoting/store/quote-request/quote-request.selectors.ts index 417bbeed82..f38b606271 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.selectors.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.selectors.ts @@ -4,79 +4,69 @@ import { isEqual } from 'lodash-es'; import { getProductEntities } from 'ish-core/store/shopping/products'; import { QuoteRequestHelper } from '../../models/quote-request/quote-request.helper'; +import { QuoteRequestData } from '../../models/quote-request/quote-request.interface'; import { getQuotingState } from '../quoting-store'; -import { initialState } from './quote-request.reducer'; +import { initialState, quoteRequestAdapter } from './quote-request.reducer'; const getQuoteRequestState = createSelector( getQuotingState, state => (state ? state.quoteRequest : initialState) ); +const { selectAll, selectEntities } = quoteRequestAdapter.getSelectors(getQuoteRequestState); + export const getSelectedQuoteRequestId = createSelector( getQuoteRequestState, state => state.selected ); +export const getSelectedQuoteRequest = createSelector( + selectEntities, + getSelectedQuoteRequestId, + (entities, id) => id && addStateToQuoteRequest(entities[id]) +); + export const getCurrentQuoteRequests = createSelector( - getQuoteRequestState, - state => - state.quoteRequests.map(item => ({ - ...item, - state: QuoteRequestHelper.getQuoteRequestState(item), - })) + selectAll, + quoteRequests => quoteRequests.map(addStateToQuoteRequest) ); -export const getQuoteRequstItems = createSelector( +export const getQuoteRequestItems = createSelector( getQuoteRequestState, state => state.quoteRequestItems ); -export const getActiveQuoteRequest = createSelector( - createSelector( - getCurrentQuoteRequests, - quoteRequests => quoteRequests.filter(item => item.editable).pop() || undefined - ), - getQuoteRequstItems, +export const getQuoteRequestItemsWithProducts = createSelector( + getQuoteRequestItems, getProductEntities, - (quoteRequest, quoteRequestItems, products) => - !quoteRequest - ? undefined - : { - ...quoteRequest, - state: QuoteRequestHelper.getQuoteRequestState(quoteRequest), - items: quoteRequestItems.map(item => ({ - ...item, - product: item.productSKU ? products[item.productSKU] : undefined, - })), - } + (items, products) => + items.map(item => ({ + ...item, + product: item.productSKU ? products[item.productSKU] : undefined, + })) +); + +export const getActiveQuoteRequest = createSelector( + getCurrentQuoteRequests, + quoteRequests => { + const quoteRequest = quoteRequests.reverse().find(item => item.editable); + return addStateToQuoteRequest(quoteRequest); + } +); + +export const getActiveQuoteRequestWithProducts = createSelector( + getActiveQuoteRequest, + getQuoteRequestItemsWithProducts, + (quoteRequest, items) => quoteRequest && { ...quoteRequest, items } ); /** * Select the selected quote request with the appended line item and product data if available. */ -export const getSelectedQuoteRequest = createSelectorFactory(projector => +export const getSelectedQuoteRequestWithProducts = createSelectorFactory(projector => defaultMemoize(projector, undefined, isEqual) -)( - createSelector( - getCurrentQuoteRequests, - getSelectedQuoteRequestId, - (items, id) => items.filter(item => item.id === id).pop() - ), - getQuoteRequstItems, - getProductEntities, - (quote, quoteRequestItems, products) => - !quote - ? undefined - : { - ...quote, - state: QuoteRequestHelper.getQuoteRequestState(quote), - items: quoteRequestItems.map(item => ({ - ...item, - product: item.productSKU ? products[item.productSKU] : undefined, - })), - } -); +)(getSelectedQuoteRequest, getQuoteRequestItemsWithProducts, (quote, items) => quote && { ...quote, items }); export const getQuoteRequestLoading = createSelector( getQuoteRequestState, @@ -87,3 +77,12 @@ export const getQuoteRequestError = createSelector( getQuoteRequestState, state => state.error ); + +function addStateToQuoteRequest(quote: QuoteRequestData) { + return ( + quote && { + ...quote, + state: QuoteRequestHelper.getQuoteRequestState(quote), + } + ); +} diff --git a/src/app/extensions/quoting/store/quote/quote.effects.ts b/src/app/extensions/quoting/store/quote/quote.effects.ts index c42b2eba30..54db6c4854 100644 --- a/src/app/extensions/quoting/store/quote/quote.effects.ts +++ b/src/app/extensions/quoting/store/quote/quote.effects.ts @@ -18,7 +18,7 @@ import { QuoteService } from '../../services/quote/quote.service'; import { QuoteRequestActionTypes } from '../quote-request'; import * as actions from './quote.actions'; -import { getSelectedQuote, getSelectedQuoteId } from './quote.selectors'; +import { getSelectedQuoteId, getSelectedQuoteWithProducts } from './quote.selectors'; @Injectable() export class QuoteEffects { @@ -81,7 +81,7 @@ export class QuoteEffects { @Effect() createQuoteRequestFromQuote$ = this.actions$.pipe( ofType(actions.QuoteActionTypes.CreateQuoteRequestFromQuote), - withLatestFrom(this.store.pipe(select(getSelectedQuote))), + withLatestFrom(this.store.pipe(select(getSelectedQuoteWithProducts))), concatMap(([, currentQuoteRequest]) => this.quoteService.createQuoteRequestFromQuote(currentQuoteRequest).pipe( map(quoteLineItemRequest => new actions.CreateQuoteRequestFromQuoteSuccess({ quoteLineItemRequest })), diff --git a/src/app/extensions/quoting/store/quote/quote.reducer.spec.ts b/src/app/extensions/quoting/store/quote/quote.reducer.spec.ts index 9b3e4f7c1a..c3f6a839f9 100644 --- a/src/app/extensions/quoting/store/quote/quote.reducer.spec.ts +++ b/src/app/extensions/quoting/store/quote/quote.reducer.spec.ts @@ -51,7 +51,8 @@ describe('Quote Reducer', () => { const action = new fromActions.LoadQuotesSuccess(quotes); const state = quoteReducer(initialState, action); - expect(state.quotes).toEqual(quotes.quotes); + expect(state.ids).toEqual(['test']); + expect(state.entities).toEqual({ test: quotes.quotes[0] }); expect(state.loading).toBeFalse(); }); }); diff --git a/src/app/extensions/quoting/store/quote/quote.reducer.ts b/src/app/extensions/quoting/store/quote/quote.reducer.ts index 1438fa9fc6..2a57a17015 100644 --- a/src/app/extensions/quoting/store/quote/quote.reducer.ts +++ b/src/app/extensions/quoting/store/quote/quote.reducer.ts @@ -1,22 +1,24 @@ +import { EntityState, createEntityAdapter } from '@ngrx/entity'; + import { HttpError } from 'ish-core/models/http-error/http-error.model'; import { QuoteData } from '../../models/quote/quote.interface'; import { QuoteAction, QuoteActionTypes } from './quote.actions'; -export interface QuoteState { - quotes: QuoteData[]; +export const quoteAdapter = createEntityAdapter(); + +export interface QuoteState extends EntityState { loading: boolean; error: HttpError; selected: string; } -export const initialState: QuoteState = { - quotes: [], +export const initialState: QuoteState = quoteAdapter.getInitialState({ loading: false, error: undefined, selected: undefined, -}; +}); export function quoteReducer(state = initialState, action: QuoteAction): QuoteState { switch (action.type) { @@ -54,10 +56,12 @@ export function quoteReducer(state = initialState, action: QuoteAction): QuoteSt case QuoteActionTypes.LoadQuotesSuccess: { const quotes = action.payload.quotes; + if (!state) { + return; + } return { - ...state, - quotes, + ...quoteAdapter.addAll(quotes, state), loading: false, }; } diff --git a/src/app/extensions/quoting/store/quote/quote.selectors.spec.ts b/src/app/extensions/quoting/store/quote/quote.selectors.spec.ts index 929dacf6bd..9aab56a138 100644 --- a/src/app/extensions/quoting/store/quote/quote.selectors.spec.ts +++ b/src/app/extensions/quoting/store/quote/quote.selectors.spec.ts @@ -16,8 +16,8 @@ import { getCurrentQuotes, getQuoteError, getQuoteLoading, - getSelectedQuote, getSelectedQuoteId, + getSelectedQuoteWithProducts, } from './quote.selectors'; describe('Quote Selectors', () => { @@ -66,7 +66,7 @@ describe('Quote Selectors', () => { }; expect(getSelectedQuoteId(store$.state)).toEqual('test'); - expect(getSelectedQuote(store$.state)).toEqual(expected); + expect(getSelectedQuoteWithProducts(store$.state)).toEqual(expected); }); }); diff --git a/src/app/extensions/quoting/store/quote/quote.selectors.ts b/src/app/extensions/quoting/store/quote/quote.selectors.ts index cfa2a4a313..0b5d263872 100644 --- a/src/app/extensions/quoting/store/quote/quote.selectors.ts +++ b/src/app/extensions/quoting/store/quote/quote.selectors.ts @@ -4,53 +4,45 @@ import { getProductEntities } from 'ish-core/store/shopping/products'; import { QuoteRequestItem } from '../../models/quote-request-item/quote-request-item.model'; import { QuoteHelper } from '../../models/quote/quote.helper'; -import { Quote } from '../../models/quote/quote.model'; +import { QuoteData } from '../../models/quote/quote.interface'; import { getQuotingState } from '../quoting-store'; -import { initialState } from './quote.reducer'; +import { initialState, quoteAdapter } from './quote.reducer'; const getQuoteState = createSelector( getQuotingState, state => (state ? state.quote : initialState) ); +const { selectAll, selectEntities } = quoteAdapter.getSelectors(getQuoteState); + export const getSelectedQuoteId = createSelector( getQuoteState, state => state.selected ); -export const getCurrentQuotes = createSelector( - getQuoteState, - state => { - const quotes: Quote[] = []; - - for (const item of state.quotes) { - quotes.push({ - ...item, - state: QuoteHelper.getQuoteState(item), - }); - } +export const getSelectedQuote = createSelector( + getSelectedQuoteId, + selectEntities, + (id, entities) => entities && id && addStateToQuote(entities[id]) +); - return quotes; - } +export const getCurrentQuotes = createSelector( + selectAll, + quotes => quotes.map(addStateToQuote) ); /** * Select the selected quote with the appended product data if available. */ -export const getSelectedQuote = createSelector( - createSelector( - getCurrentQuotes, - getSelectedQuoteId, - (items, id) => items.filter(item => item.id === id).pop() - ), +export const getSelectedQuoteWithProducts = createSelector( + getSelectedQuote, getProductEntities, (quote, products) => !quote ? undefined : { ...quote, - state: QuoteHelper.getQuoteState(quote), items: quote.items.map((item: QuoteRequestItem) => ({ ...item, product: item.productSKU ? products[item.productSKU] : undefined, @@ -67,3 +59,12 @@ export const getQuoteError = createSelector( getQuoteState, state => state.error ); + +function addStateToQuote(quote: QuoteData) { + return ( + quote && { + ...quote, + state: QuoteHelper.getQuoteState(quote), + } + ); +}