diff --git a/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts index e833df15f6..d03bf9d653 100644 --- a/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts +++ b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts @@ -55,14 +55,14 @@ export class PublicationClaimComponent implements OnInit { /** * The source for which to list targets */ - @Input() source: string; + @Input() source = ''; /** * The pagination system configuration for HTML listing. * @type {PaginationComponentOptions} */ public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'stp', + id: 'stp_' + this.source, pageSizeOptions: [5, 10, 20, 40, 60], }); @@ -99,11 +99,16 @@ export class PublicationClaimComponent implements OnInit { * Component initialization. */ ngOnInit(): void { - this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(); - this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(); + this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(this.source); + this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(this.source); + } + /** + * First Suggestion Targets loading after view initialization. + */ + ngAfterViewInit(): void { this.subs.push( - this.suggestionTargetsStateService.isSuggestionTargetsLoaded().pipe( + this.suggestionTargetsStateService.isSuggestionTargetsLoaded(this.source).pipe( take(1), ).subscribe(() => { this.getSuggestionTargets(); @@ -118,7 +123,7 @@ export class PublicationClaimComponent implements OnInit { * 'true' if the targets are loading, 'false' otherwise. */ public isTargetsLoading(): Observable { - return this.suggestionTargetsStateService.isSuggestionTargetsLoading(); + return this.suggestionTargetsStateService.isSuggestionTargetsLoading(this.source); } /** @@ -128,7 +133,7 @@ export class PublicationClaimComponent implements OnInit { * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ public isTargetsProcessing(): Observable { - return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(); + return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(this.source); } /** @@ -145,7 +150,7 @@ export class PublicationClaimComponent implements OnInit { * Unsubscribe from all subscriptions. */ ngOnDestroy(): void { - this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); + this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.source); this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); diff --git a/src/app/notifications/suggestion-targets/selectors.ts b/src/app/notifications/suggestion-targets/selectors.ts new file mode 100644 index 0000000000..f80e36788b --- /dev/null +++ b/src/app/notifications/suggestion-targets/selectors.ts @@ -0,0 +1,116 @@ +import { + createFeatureSelector, + createSelector, + MemoizedSelector, +} from '@ngrx/store'; + +import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model'; +import { subStateSelector } from '../../submission/selectors'; +import { + suggestionNotificationsSelector, + SuggestionNotificationsState, +} from '../notifications.reducer'; +import { + SuggestionTargetEntry, + SuggestionTargetState, +} from './suggestion-targets.reducer'; + +/** + * Returns the Reciter Suggestion Target state. + * @function _getSuggestionTargetState + * @param {AppState} state Top level state. + * @return {SuggestionNotificationsState} + */ +const _getSuggestionTargetState = createFeatureSelector('suggestionNotifications'); + +// Suggestion Targets selectors + +/** + * Returns the Suggestion Targets State. + * @function suggestionTargetStateSelector + * @return {SuggestionNotificationsState} + */ +export function suggestionTargetStateSelector(): MemoizedSelector { + return subStateSelector(suggestionNotificationsSelector, 'suggestionTarget'); +} + +/** + * Returns the Reciter Suggestion source state + * @function suggestionSourceSelector + * @return {SuggestionTargetEntry} + */ +export function suggestionSourceSelector(source: string): MemoizedSelector { + return createSelector(suggestionTargetStateSelector(),(state: SuggestionTargetState) => state.sources[source]); +} + +/** + * Returns the Suggestion Targets list by source. + * @function suggestionTargetObjectSelector + * @return {SuggestionTarget[]} + */ +export function suggestionTargetObjectSelector(source: string): MemoizedSelector { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state.targets); +} + +/** + * Returns true if the Suggestion Targets are loaded. + * @function isSuggestionTargetLoadedSelector + * @return {boolean} + */ +export const isSuggestionTargetLoadedSelector = (source: string) => { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.loaded || false); +}; + +/** + * Returns true if the deduplication sets are processing. + * @function isSuggestionTargetProcessingSelector + * @return {boolean} + */ +export const isSuggestionTargetProcessingSelector = (source: string) => { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.processing || false); +}; + +/** + * Returns the total available pages of Reciter Suggestion Targets. + * @function getSuggestionTargetTotalPagesSelector + * @return {number} + */ +export const getSuggestionTargetTotalPagesSelector = (source: string) => { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalPages || 0); +}; + +/** + * Returns the current page of Suggestion Targets. + * @function getSuggestionTargetCurrentPageSelector + * @return {number} + */ +export const getSuggestionTargetCurrentPageSelector = (source: string) => { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.currentPage || 0); +}; + +/** + * Returns the total number of Suggestion Targets. + * @function getSuggestionTargetTotalsSelector + * @return {number} + */ +export const getSuggestionTargetTotalsSelector = (source: string) => { + return createSelector(suggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalElements || 0); +}; + +/** + * Returns Suggestion Targets for the current user. + * @function getCurrentUserSuggestionTargetsSelector + * @return {SuggestionTarget[]} + */ +export const getCurrentUserSuggestionTargetsSelector = () => { + return createSelector(suggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargets || []); +}; + +/** + * Returns whether or not the user has consulted their suggestions + * @function getCurrentUserSuggestionTargetsVisitedSelector + * @return {boolean} + */ +export const getCurrentUserSuggestionTargetsVisitedSelector = () => { + return createSelector(suggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargetsVisited || false); +}; diff --git a/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts b/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts index a4e0d0e027..ee2009e144 100644 --- a/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts @@ -23,10 +23,8 @@ export const SuggestionTargetActionTypes = { MARK_USER_SUGGESTIONS_AS_VISITED: type('dspace/integration/openaire/suggestions/target/MARK_USER_SUGGESTIONS_AS_VISITED'), }; -/* tslint:disable:max-classes-per-file */ - /** - * An ngrx action to retrieve all the Suggestion Targets. + * A ngrx action to retrieve all the Suggestion Targets. */ export class RetrieveTargetsBySourceAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; @@ -56,18 +54,34 @@ export class RetrieveTargetsBySourceAction implements Action { } /** - * An ngrx action for retrieving 'all Suggestion Targets' error. + * A ngrx action for notifying error. */ -export class RetrieveAllTargetsErrorAction implements Action { +export class RetrieveTargetsBySourceErrorAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR; + payload: { + source: string; + }; + + /** + * Create a new RetrieveTargetsBySourceAction. + * + * @param source + * the source for which to retrieve suggestion targets + */ + constructor(source: string) { + this.payload = { + source, + }; + } } /** - * An ngrx action to load the Suggestion Target objects. + * A ngrx action to load the Suggestion Target objects. */ export class AddTargetAction implements Action { type = SuggestionTargetActionTypes.ADD_TARGETS; payload: { + source: string; targets: SuggestionTarget[]; totalPages: number; currentPage: number; @@ -77,6 +91,8 @@ export class AddTargetAction implements Action { /** * Create a new AddTargetAction. * + * @param source + * the source of suggestion targets * @param targets * the list of targets * @param totalPages @@ -86,8 +102,9 @@ export class AddTargetAction implements Action { * @param totalElements * the total available Suggestion Targets */ - constructor(targets: SuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + constructor(source: string, targets: SuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { this.payload = { + source, targets, totalPages, currentPage, @@ -98,7 +115,7 @@ export class AddTargetAction implements Action { } /** - * An ngrx action to load the user Suggestion Target object. + * A ngrx action to load the user Suggestion Target object. * Called by the ??? effect. */ export class AddUserSuggestionsAction implements Action { @@ -120,7 +137,7 @@ export class AddUserSuggestionsAction implements Action { } /** - * An ngrx action to reload the user Suggestion Target object. + * A ngrx action to reload the user Suggestion Target object. * Called by the ??? effect. */ export class RefreshUserSuggestionsAction implements Action { @@ -135,7 +152,7 @@ export class RefreshUserSuggestionsErrorAction implements Action { } /** - * An ngrx action to Mark User Suggestions As Visited. + * A ngrx action to Mark User Suggestions As Visited. * Called by the ??? effect. */ export class MarkUserSuggestionsAsVisitedAction implements Action { @@ -143,13 +160,26 @@ export class MarkUserSuggestionsAsVisitedAction implements Action { } /** - * An ngrx action to clear targets state. + * A ngrx action to clear targets state. */ export class ClearSuggestionTargetsAction implements Action { type = SuggestionTargetActionTypes.CLEAR_TARGETS; -} + payload: { + source: string; + }; -/* tslint:enable:max-classes-per-file */ + /** + * Create a new ClearSuggestionTargetsAction. + * + * @param source + * the source of suggestion targets + */ + constructor(source: string) { + this.payload = { + source, + }; + } +} /** * Export a type alias of all actions in this action group @@ -161,5 +191,5 @@ export type SuggestionTargetsActions | ClearSuggestionTargetsAction | MarkUserSuggestionsAsVisitedAction | RetrieveTargetsBySourceAction - | RetrieveAllTargetsErrorAction + | RetrieveTargetsBySourceErrorAction | RefreshUserSuggestionsAction; diff --git a/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts b/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts index e3956cbbdc..de44e2489e 100644 --- a/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts @@ -14,7 +14,12 @@ import { tap, } from 'rxjs/operators'; +import { + AuthActionTypes, + RetrieveAuthenticatedEpersonSuccessAction, +} from '../../core/auth/auth.actions'; import { PaginatedList } from '../../core/data/paginated-list.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { SuggestionsService } from '../suggestions.service'; @@ -22,8 +27,8 @@ import { AddTargetAction, AddUserSuggestionsAction, RefreshUserSuggestionsErrorAction, - RetrieveAllTargetsErrorAction, RetrieveTargetsBySourceAction, + RetrieveTargetsBySourceErrorAction, SuggestionTargetActionTypes, } from './suggestion-targets.actions'; @@ -45,13 +50,13 @@ export class SuggestionTargetsEffects { action.payload.currentPage, ).pipe( map((targets: PaginatedList) => - new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements), + new AddTargetAction(action.payload.source, targets.page, targets.totalPages, targets.currentPage, targets.totalElements), ), catchError((error: unknown) => { if (error instanceof Error) { console.error(error.message); } - return of(new RetrieveAllTargetsErrorAction()); + return of(new RetrieveTargetsBySourceErrorAction(action.payload.source)); }), ); }), @@ -67,16 +72,27 @@ export class SuggestionTargetsEffects { }), ), { dispatch: false }); + /** + * Show a notification on error. + */ + retrieveUserTargets$ = createEffect(() => this.actions$.pipe( + ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS), + switchMap((action: RetrieveAuthenticatedEpersonSuccessAction) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(action.payload).pipe( + map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), + ); + }))); + /** * Fetch the current user suggestion */ refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe( ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), switchMap(() => { - return this.store$.select((state: any) => state.core.auth.userId) + return this.store$.select((state: any) => state.core.auth.user) .pipe( - switchMap((userId: string) => { - return this.suggestionsService.retrieveCurrentUserSuggestions(userId) + switchMap((user: EPerson) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(user.uuid) .pipe( map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), catchError((error: unknown) => { diff --git a/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts b/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts index 16bd7b0b7b..74f8ec2f5c 100644 --- a/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts @@ -7,13 +7,21 @@ import { /** * The interface representing the OpenAIRE suggestion targets state. */ -export interface SuggestionTargetState { +export interface SuggestionTargetEntry { targets: SuggestionTarget[]; processing: boolean; loaded: boolean; totalPages: number; currentPage: number; totalElements: number; +} + +export interface SuggestionSourcesState { + [source: string]: SuggestionTargetEntry; +} + +export interface SuggestionTargetState { + sources: SuggestionSourcesState; currentUserTargets: SuggestionTarget[]; currentUserTargetsVisited: boolean; } @@ -21,13 +29,17 @@ export interface SuggestionTargetState { /** * Used for the OpenAIRE Suggestion Target state initialization. */ -const SuggestionTargetInitialState: SuggestionTargetState = { +const suggestionSourceTargetsInitialState: SuggestionTargetEntry = { targets: [], processing: false, loaded: false, totalPages: 0, currentPage: 0, totalElements: 0, +}; + +const SuggestionTargetInitialState: SuggestionTargetState = { + sources: {}, currentUserTargets: null, currentUserTargetsVisited: false, }; @@ -45,25 +57,42 @@ const SuggestionTargetInitialState: SuggestionTargetState = { export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { switch (action.type) { case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: true, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState, + }), + }); } case SuggestionTargetActionTypes.ADD_TARGETS: { - return Object.assign({}, state, { - targets: state.targets.concat(action.payload.targets), + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { + targets: sourceState.targets.concat(action.payload.targets), processing: false, loaded: true, totalPages: action.payload.totalPages, - currentPage: state.currentPage, + currentPage: action.payload.currentPage, totalElements: action.payload.totalElements, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState, + }), + }); } case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, loaded: true, @@ -71,6 +100,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState, + }), + }); } case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { @@ -86,7 +122,8 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a } case SuggestionTargetActionTypes.CLEAR_TARGETS: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, loaded: false, @@ -94,6 +131,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState, + }), + }); } default: { diff --git a/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts b/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts index 25115f794c..413a3b82ea 100644 --- a/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts @@ -7,16 +7,16 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model'; +import { SuggestionNotificationsState } from '../notifications.reducer'; import { getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsVisitedSelector, getSuggestionTargetCurrentPageSelector, getSuggestionTargetTotalsSelector, - isReciterSuggestionTargetProcessingSelector, isSuggestionTargetLoadedSelector, + isSuggestionTargetProcessingSelector, suggestionTargetObjectSelector, -} from '../../suggestion-notifications/selectors'; -import { SuggestionNotificationsState } from '../notifications.reducer'; +} from './selectors'; import { ClearSuggestionTargetsAction, MarkUserSuggestionsAsVisitedAction, @@ -42,8 +42,8 @@ export class SuggestionTargetsStateService { * @return Observable * The list of Suggestion Targets. */ - public getSuggestionTargets(): Observable { - return this.store.pipe(select(suggestionTargetObjectSelector())); + public getSuggestionTargets(source: string): Observable { + return this.store.pipe(select(suggestionTargetObjectSelector(source))); } /** @@ -52,9 +52,9 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loading, 'false' otherwise. */ - public isSuggestionTargetsLoading(): Observable { + public isSuggestionTargetsLoading(source: string): Observable { return this.store.pipe( - select(isSuggestionTargetLoadedSelector), + select(isSuggestionTargetLoadedSelector(source)), map((loaded: boolean) => !loaded), ); } @@ -65,8 +65,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loaded, 'false' otherwise. */ - public isSuggestionTargetsLoaded(): Observable { - return this.store.pipe(select(isSuggestionTargetLoadedSelector)); + public isSuggestionTargetsLoaded(source: string): Observable { + return this.store.pipe(select(isSuggestionTargetLoadedSelector(source))); } /** @@ -75,8 +75,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ - public isSuggestionTargetsProcessing(): Observable { - return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector)); + public isSuggestionTargetsProcessing(source: string): Observable { + return this.store.pipe(select(isSuggestionTargetProcessingSelector(source))); } /** @@ -85,8 +85,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Suggestion Targets pages. */ - public getSuggestionTargetsTotalPages(): Observable { - return this.store.pipe(select(getSuggestionTargetTotalsSelector)); + public getSuggestionTargetsTotalPages(source: string): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector(source))); } /** @@ -95,8 +95,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the current Suggestion Targets page. */ - public getSuggestionTargetsCurrentPage(): Observable { - return this.store.pipe(select(getSuggestionTargetCurrentPageSelector)); + public getSuggestionTargetsCurrentPage(source: string): Observable { + return this.store.pipe(select(getSuggestionTargetCurrentPageSelector(source))); } /** @@ -105,8 +105,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Suggestion Targets. */ - public getSuggestionTargetsTotals(): Observable { - return this.store.pipe(select(getSuggestionTargetTotalsSelector)); + public getSuggestionTargetsTotals(source: string): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector(source))); } /** @@ -130,7 +130,7 @@ export class SuggestionTargetsStateService { * The Suggestion Targets object. */ public getCurrentUserSuggestionTargets(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector())); } /** @@ -140,7 +140,7 @@ export class SuggestionTargetsStateService { * True if user already visited, false otherwise. */ public hasUserVisitedSuggestions(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector())); } /** @@ -152,9 +152,12 @@ export class SuggestionTargetsStateService { /** * Dispatch an action to clear the Reciter Suggestion Targets state. + * + * @param source + * the source of suggestion targets */ - public dispatchClearSuggestionTargetsAction(): void { - this.store.dispatch(new ClearSuggestionTargetsAction()); + public dispatchClearSuggestionTargetsAction(source: string): void { + this.store.dispatch(new ClearSuggestionTargetsAction(source)); } /** diff --git a/src/app/notifications/suggestions-notification/suggestions-notification.component.ts b/src/app/notifications/suggestions-notification/suggestions-notification.component.ts index 3a8d497b82..568cafb9de 100644 --- a/src/app/notifications/suggestions-notification/suggestions-notification.component.ts +++ b/src/app/notifications/suggestions-notification/suggestions-notification.component.ts @@ -44,8 +44,8 @@ export class SuggestionsNotificationComponent implements OnInit { ) { } ngOnInit() { - this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.suggestionsRD$ = this.suggestionTargetsStateService.getCurrentUserSuggestionTargets(); + this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } /** diff --git a/src/app/notifications/suggestions-popup/suggestions-popup.component.ts b/src/app/notifications/suggestions-popup/suggestions-popup.component.ts index 07fd7d2e2d..d2256c789d 100644 --- a/src/app/notifications/suggestions-popup/suggestions-popup.component.ts +++ b/src/app/notifications/suggestions-popup/suggestions-popup.component.ts @@ -16,6 +16,7 @@ import { Observable, of, Subject, + Subscription, } from 'rxjs'; import { take, @@ -53,7 +54,7 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy { labelPrefix = 'notification.'; - subscription; + subscription: Subscription; suggestionsRD$: Observable; diff --git a/src/app/notifications/suggestion.service.spec.ts b/src/app/notifications/suggestions.service.spec.ts similarity index 98% rename from src/app/notifications/suggestion.service.spec.ts rename to src/app/notifications/suggestions.service.spec.ts index 2b5bc48f3b..7a552d3ce4 100644 --- a/src/app/notifications/suggestion.service.spec.ts +++ b/src/app/notifications/suggestions.service.spec.ts @@ -16,6 +16,7 @@ import { ResourceType } from '../core/shared/resource-type'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { mockSuggestionPublicationOne } from '../shared/mocks/publication-claim.mock'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { followLink } from '../shared/utils/follow-link-config.model'; import { SuggestionsService } from './suggestions.service'; describe('SuggestionsService test', () => { @@ -115,7 +116,7 @@ describe('SuggestionsService test', () => { it('should retrieve current user suggestions', () => { service.retrieveCurrentUserSuggestions('1234'); - expect(researcherProfileService.findById).toHaveBeenCalledWith('1234', true); + expect(researcherProfileService.findById).toHaveBeenCalledWith('1234', true, true, followLink('item')); }); it('should approve and import suggestion', () => { diff --git a/src/app/notifications/suggestions.service.ts b/src/app/notifications/suggestions.service.ts index e37aa728a2..f1790f830f 100644 --- a/src/app/notifications/suggestions.service.ts +++ b/src/app/notifications/suggestions.service.ts @@ -29,7 +29,6 @@ import { ResearcherProfile } from '../core/profile/model/researcher-profile.mode import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service'; import { NoContent } from '../core/shared/NoContent.model'; import { - getAllSucceededRemoteDataPayload, getFinishedRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, @@ -42,6 +41,7 @@ import { hasValue, isNotEmpty, } from '../shared/empty.util'; +import { followLink } from '../shared/utils/follow-link-config.model'; import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths'; /** @@ -121,7 +121,7 @@ export class SuggestionsService { * @return Observable>> * The list of Suggestion. */ - public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable> { + public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable>> { const [source, target] = targetId.split(':'); const findListOptions: FindListOptions = { @@ -130,9 +130,7 @@ export class SuggestionsService { sort: sortOptions, }; - return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions).pipe( - getAllSucceededRemoteDataPayload(), - ); + return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions); } /** @@ -169,7 +167,7 @@ export class SuggestionsService { if (hasNoValue(userUuid)) { return of([]); } - return this.researcherProfileService.findById(userUuid, true).pipe( + return this.researcherProfileService.findById(userUuid, true, true, followLink('item')).pipe( getFirstCompletedRemoteData(), mergeMap((profile: RemoteData ) => { if (isNotEmpty(profile) && profile.hasSucceeded && isNotEmpty(profile.payload)) { diff --git a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.html b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.html index 11db25fa7b..b04e7132f1 100644 --- a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.html +++ b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/mocks/suggestion.mock.ts b/src/app/shared/mocks/suggestion.mock.ts index 90226841ef..ff98426c37 100644 --- a/src/app/shared/mocks/suggestion.mock.ts +++ b/src/app/shared/mocks/suggestion.mock.ts @@ -2,7 +2,9 @@ import { of as observableOf } from 'rxjs'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Item } from '../../core/shared/item.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { SearchResult } from '../search/models/search-result.model'; +import { createPaginatedList } from '../testing/utils.test'; // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- @@ -1345,7 +1347,7 @@ export function getMockSuggestionNotificationsStateService(): any { export function getMockSuggestionsService(): any { return jasmine.createSpyObj('SuggestionsService', { getTargets: jasmine.createSpy('getTargets'), - getSuggestions: observableOf([]), + getSuggestions: createSuccessfulRemoteDataObject$(createPaginatedList([])), clearSuggestionRequests: jasmine.createSpy('clearSuggestionRequests'), deleteReviewedSuggestion: jasmine.createSpy('deleteReviewedSuggestion'), retrieveCurrentUserSuggestions: jasmine.createSpy('retrieveCurrentUserSuggestions'), diff --git a/src/app/suggestion-notifications/selectors.ts b/src/app/suggestion-notifications/selectors.ts deleted file mode 100644 index b44b0877da..0000000000 --- a/src/app/suggestion-notifications/selectors.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - createFeatureSelector, - createSelector, - MemoizedSelector, -} from '@ngrx/store'; - -import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; -import { - suggestionNotificationsSelector, - SuggestionNotificationsState, -} from '../notifications/notifications.reducer'; -import { SuggestionTargetState } from '../notifications/suggestion-targets/suggestion-targets.reducer'; -import { subStateSelector } from '../submission/selectors'; - -/** - * Returns the Reciter Suggestion Target state. - * @function _getSuggestionTargetState - * @param {AppState} state Top level state. - * @return {SuggestionNotificationsState} - */ -const _getSuggestionTargetState = createFeatureSelector('suggestionNotifications'); - -// Reciter Suggestion Targets -// ---------------------------------------------------------------------------- - -/** - * Returns the Suggestion Targets State. - * @function suggestionTargetStateSelector - * @return {SuggestionNotificationsState} - */ -export function suggestionTargetStateSelector(): MemoizedSelector { - return subStateSelector(suggestionNotificationsSelector, 'suggestionTarget'); -} - -/** - * Returns the Suggestion Targets list. - * @function suggestionTargetObjectSelector - * @return {SuggestionTarget[]} - */ -export function suggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(suggestionTargetStateSelector(), 'targets'); -} - -/** - * Returns true if the Suggestion Targets are loaded. - * @function isSuggestionTargetLoadedSelector - * @return {boolean} - */ -export const isSuggestionTargetLoadedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.loaded, -); - -/** - * Returns true if the deduplication sets are processing. - * @function isDeduplicationSetsProcessingSelector - * @return {boolean} - */ -export const isReciterSuggestionTargetProcessingSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.processing, -); - -/** - * Returns the total available pages of Reciter Suggestion Targets. - * @function getSuggestionTargetTotalPagesSelector - * @return {number} - */ -export const getSuggestionTargetTotalPagesSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalPages, -); - -/** - * Returns the current page of Suggestion Targets. - * @function getSuggestionTargetCurrentPageSelector - * @return {number} - */ -export const getSuggestionTargetCurrentPageSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentPage, -); - -/** - * Returns the total number of Suggestion Targets. - * @function getSuggestionTargetTotalsSelector - * @return {number} - */ -export const getSuggestionTargetTotalsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalElements, -); - -/** - * Returns Suggestion Targets for the current user. - * @function getCurrentUserSuggestionTargetSelector - * @return {SuggestionTarget[]} - */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets, -); - -/** - * Returns whether or not the user has consulted their suggestions - * @function getCurrentUserSuggestionTargetSelector - * @return {boolean} - */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited, -); diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html index 75ba0315f3..45bdff32bf 100644 --- a/src/app/suggestions-page/suggestions-page.component.html +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -2,7 +2,8 @@
-
+ +

{{'suggestion.suggestionFor' | translate}} @@ -21,7 +22,6 @@ (ignoreSuggestionClicked)="ignoreSuggestionAllSelected()">

-
-
{{ 'suggestion.count.missing' | translate }}
+ + {{'suggestion.count.missing' | translate}} +
diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts index 8da48d740f..6c19405e94 100644 --- a/src/app/suggestions-page/suggestions-page.component.spec.ts +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -1,11 +1,10 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { - async, ComponentFixture, fakeAsync, TestBed, - tick, + waitForAsync, } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { @@ -70,7 +69,7 @@ describe('SuggestionPageComponent', () => { }); const paginationService = new PaginationServiceStub(); - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ BrowserModule, @@ -106,7 +105,7 @@ describe('SuggestionPageComponent', () => { }); it('should create', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); scheduler.schedule(() => fixture.detectChanges()); scheduler.flush(); @@ -118,70 +117,72 @@ describe('SuggestionPageComponent', () => { }); it('should update page on pagination change', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.onPaginationChange()); scheduler.flush(); - component.onPaginationChange(); + expect(component.updatePage).toHaveBeenCalled(); }); - it('should update suggestion on page update', (done) => { + it('should update suggestion on page update', () => { spyOn(component.processing$, 'next'); spyOn(component.suggestionsRD$, 'next'); - scheduler.schedule(() => fixture.detectChanges()); + component.targetId$ = observableOf('testid'); + scheduler.schedule(() => component.updatePage().subscribe()); scheduler.flush(); - paginationService.getFindListOptions().subscribe(() => { - expect(component.processing$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); - expect(component.suggestionsRD$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); - done(); - }); - component.updatePage(); + + expect(component.processing$.next).toHaveBeenCalledTimes(2); + expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); + expect(component.suggestionsRD$.next).toHaveBeenCalled(); + expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); }); it('should flag suggestion for deletion', fakeAsync(() => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestion('1')); scheduler.flush(); - component.ignoreSuggestion('1'); + expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); - tick(201); expect(component.updatePage).toHaveBeenCalled(); })); it('should flag all suggestion for deletion', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestionAllSelected()); scheduler.flush(); - component.ignoreSuggestionAllSelected(); + expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import multiple suggestions', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index e5dfe5a2ce..0fc790a125 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -26,7 +26,7 @@ import { distinctUntilChanged, map, switchMap, - take, + tap, } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -41,7 +41,10 @@ import { Suggestion } from '../core/notifications/suggestions/models/suggestion. import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; import { PaginationService } from '../core/pagination/pagination.service'; import { redirectOn4xx } from '../core/shared/authorized.operators'; -import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../core/shared/operators'; import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component'; @@ -52,6 +55,7 @@ import { SuggestionBulkResult, SuggestionsService, } from '../notifications/suggestions.service'; +import { AlertComponent } from '../shared/alert/alert.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { PaginationComponent } from '../shared/pagination/pagination.component'; @@ -74,6 +78,7 @@ import { getWorkspaceItemEditRoute } from '../workflowitems-edit-page/workflowit PaginationComponent, SuggestionListElementComponent, NgForOf, + AlertComponent, ], standalone: true, }) @@ -149,14 +154,15 @@ export class SuggestionsPageComponent implements OnInit { ); this.targetRD$.pipe( getFirstSucceededRemoteDataPayload(), - ).subscribe((suggestionTarget: SuggestionTarget) => { - this.suggestionTarget = suggestionTarget; - this.suggestionId = suggestionTarget.id; - this.researcherName = suggestionTarget.display; - this.suggestionSource = suggestionTarget.source; - this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); - this.updatePage(); - }); + tap((suggestionTarget: SuggestionTarget) => { + this.suggestionTarget = suggestionTarget; + this.suggestionId = suggestionTarget.id; + this.researcherName = suggestionTarget.display; + this.suggestionSource = suggestionTarget.source; + this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); + }), + switchMap(() => this.updatePage()), + ).subscribe(); this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } @@ -165,13 +171,13 @@ export class SuggestionsPageComponent implements OnInit { * Called when one of the pagination settings is changed */ onPaginationChange() { - this.updatePage(); + this.updatePage().subscribe(); } /** * Update the list of suggestions */ - updatePage() { + updatePage(): Observable>> { this.processing$.next(true); const pageConfig$: Observable = this.paginationService.getFindListOptions( this.paginationOptions.id, @@ -179,7 +185,8 @@ export class SuggestionsPageComponent implements OnInit { ).pipe( distinctUntilChanged(), ); - combineLatest([this.targetId$, pageConfig$]).pipe( + + return combineLatest([this.targetId$, pageConfig$]).pipe( switchMap(([targetId, config]: [string, FindListOptions]) => { return this.suggestionService.getSuggestions( targetId, @@ -188,12 +195,18 @@ export class SuggestionsPageComponent implements OnInit { config.sort, ); }), - take(1), - ).subscribe((results: PaginatedList) => { - this.processing$.next(false); - this.suggestionsRD$.next(results); - this.suggestionService.clearSuggestionRequests(); - }); + getFirstCompletedRemoteData(), + tap((resultsRD: RemoteData>) => { + this.processing$.next(false); + if (resultsRD.hasSucceeded) { + this.suggestionsRD$.next(resultsRD.payload); + } else { + this.suggestionsRD$.next(null); + } + + this.suggestionService.clearSuggestionRequests(); + }), + ); } /** @@ -201,11 +214,10 @@ export class SuggestionsPageComponent implements OnInit { * @suggestionId */ ignoreSuggestion(suggestionId) { - this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { - this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - //We add a little delay in the page refresh so that we ensure the deletion has been propagated - setTimeout(() => this.updatePage(), 200); - }); + this.suggestionService.ignoreSuggestion(suggestionId).pipe( + tap(() => this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction()), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -213,11 +225,9 @@ export class SuggestionsPageComponent implements OnInit { */ ignoreSuggestionAllSelected() { this.isBulkOperationPending = true; - this.suggestionService - .ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -230,7 +240,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.ignoreSuggestion.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -238,13 +250,14 @@ export class SuggestionsPageComponent implements OnInit { * @param event contains the suggestion and the target collection */ approveAndImport(event: SuggestionApproveAndImport) { - this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) - .subscribe((workspaceitem: WorkspaceItem) => { + this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId).pipe( + tap((workspaceitem: WorkspaceItem) => { const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); this.notificationService.success('', content, { timeOut:0 }, true); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -253,11 +266,9 @@ export class SuggestionsPageComponent implements OnInit { */ approveAndImportAllSelected(event: SuggestionApproveAndImport) { this.isBulkOperationPending = true; - this.suggestionService - .approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -270,7 +281,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.approveAndImport.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index ef9bb3e55b..1314846b6e 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -5,12 +5,11 @@ import { RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service'; -import { hasValue } from '../shared/empty.util'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * Method for resolving a suggestion target based on the parameters in the current route @@ -26,6 +25,6 @@ export const suggestionsPageResolver: ResolveFn> = suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), ): Observable> => { return suggestionsDataService.getTargetById(route.params.targetId).pipe( - find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + getFirstCompletedRemoteData(), ); };