[DURACOM-271] Fix suggestion list pagination and add support for multiple sources

This commit is contained in:
Giuseppe Digilio
2024-06-12 17:47:40 +02:00
parent ecfe7e9fa0
commit fb55ad400c
17 changed files with 373 additions and 247 deletions

View File

@@ -55,14 +55,14 @@ export class PublicationClaimComponent implements OnInit {
/** /**
* The source for which to list targets * The source for which to list targets
*/ */
@Input() source: string; @Input() source = '';
/** /**
* The pagination system configuration for HTML listing. * The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions} * @type {PaginationComponentOptions}
*/ */
public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'stp', id: 'stp_' + this.source,
pageSizeOptions: [5, 10, 20, 40, 60], pageSizeOptions: [5, 10, 20, 40, 60],
}); });
@@ -99,11 +99,16 @@ export class PublicationClaimComponent implements OnInit {
* Component initialization. * Component initialization.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(); this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(this.source);
this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(); this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(this.source);
}
/**
* First Suggestion Targets loading after view initialization.
*/
ngAfterViewInit(): void {
this.subs.push( this.subs.push(
this.suggestionTargetsStateService.isSuggestionTargetsLoaded().pipe( this.suggestionTargetsStateService.isSuggestionTargetsLoaded(this.source).pipe(
take(1), take(1),
).subscribe(() => { ).subscribe(() => {
this.getSuggestionTargets(); this.getSuggestionTargets();
@@ -118,7 +123,7 @@ export class PublicationClaimComponent implements OnInit {
* 'true' if the targets are loading, 'false' otherwise. * 'true' if the targets are loading, 'false' otherwise.
*/ */
public isTargetsLoading(): Observable<boolean> { public isTargetsLoading(): Observable<boolean> {
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. * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
*/ */
public isTargetsProcessing(): Observable<boolean> { public isTargetsProcessing(): Observable<boolean> {
return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(); return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(this.source);
} }
/** /**
@@ -145,7 +150,7 @@ export class PublicationClaimComponent implements OnInit {
* Unsubscribe from all subscriptions. * Unsubscribe from all subscriptions.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.source);
this.subs this.subs
.filter((sub) => hasValue(sub)) .filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe()); .forEach((sub) => sub.unsubscribe());

View File

@@ -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<SuggestionNotificationsState>('suggestionNotifications');
// Suggestion Targets selectors
/**
* Returns the Suggestion Targets State.
* @function suggestionTargetStateSelector
* @return {SuggestionNotificationsState}
*/
export function suggestionTargetStateSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTargetState> {
return subStateSelector<SuggestionNotificationsState, SuggestionTargetState>(suggestionNotificationsSelector, 'suggestionTarget');
}
/**
* Returns the Reciter Suggestion source state
* @function suggestionSourceSelector
* @return {SuggestionTargetEntry}
*/
export function suggestionSourceSelector(source: string): MemoizedSelector<SuggestionNotificationsState, SuggestionTargetEntry> {
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<SuggestionNotificationsState, SuggestionTarget[]> {
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);
};

View File

@@ -23,10 +23,8 @@ export const SuggestionTargetActionTypes = {
MARK_USER_SUGGESTIONS_AS_VISITED: type('dspace/integration/openaire/suggestions/target/MARK_USER_SUGGESTIONS_AS_VISITED'), 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 { export class RetrieveTargetsBySourceAction implements Action {
type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; 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; 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 { export class AddTargetAction implements Action {
type = SuggestionTargetActionTypes.ADD_TARGETS; type = SuggestionTargetActionTypes.ADD_TARGETS;
payload: { payload: {
source: string;
targets: SuggestionTarget[]; targets: SuggestionTarget[];
totalPages: number; totalPages: number;
currentPage: number; currentPage: number;
@@ -77,6 +91,8 @@ export class AddTargetAction implements Action {
/** /**
* Create a new AddTargetAction. * Create a new AddTargetAction.
* *
* @param source
* the source of suggestion targets
* @param targets * @param targets
* the list of targets * the list of targets
* @param totalPages * @param totalPages
@@ -86,8 +102,9 @@ export class AddTargetAction implements Action {
* @param totalElements * @param totalElements
* the total available Suggestion Targets * 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 = { this.payload = {
source,
targets, targets,
totalPages, totalPages,
currentPage, 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. * Called by the ??? effect.
*/ */
export class AddUserSuggestionsAction implements Action { 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. * Called by the ??? effect.
*/ */
export class RefreshUserSuggestionsAction implements Action { 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. * Called by the ??? effect.
*/ */
export class MarkUserSuggestionsAsVisitedAction implements Action { 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 { export class ClearSuggestionTargetsAction implements Action {
type = SuggestionTargetActionTypes.CLEAR_TARGETS; 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 * Export a type alias of all actions in this action group
@@ -161,5 +191,5 @@ export type SuggestionTargetsActions
| ClearSuggestionTargetsAction | ClearSuggestionTargetsAction
| MarkUserSuggestionsAsVisitedAction | MarkUserSuggestionsAsVisitedAction
| RetrieveTargetsBySourceAction | RetrieveTargetsBySourceAction
| RetrieveAllTargetsErrorAction | RetrieveTargetsBySourceErrorAction
| RefreshUserSuggestionsAction; | RefreshUserSuggestionsAction;

View File

@@ -14,7 +14,12 @@ import {
tap, tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import {
AuthActionTypes,
RetrieveAuthenticatedEpersonSuccessAction,
} from '../../core/auth/auth.actions';
import { PaginatedList } from '../../core/data/paginated-list.model'; 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 { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SuggestionsService } from '../suggestions.service'; import { SuggestionsService } from '../suggestions.service';
@@ -22,8 +27,8 @@ import {
AddTargetAction, AddTargetAction,
AddUserSuggestionsAction, AddUserSuggestionsAction,
RefreshUserSuggestionsErrorAction, RefreshUserSuggestionsErrorAction,
RetrieveAllTargetsErrorAction,
RetrieveTargetsBySourceAction, RetrieveTargetsBySourceAction,
RetrieveTargetsBySourceErrorAction,
SuggestionTargetActionTypes, SuggestionTargetActionTypes,
} from './suggestion-targets.actions'; } from './suggestion-targets.actions';
@@ -45,13 +50,13 @@ export class SuggestionTargetsEffects {
action.payload.currentPage, action.payload.currentPage,
).pipe( ).pipe(
map((targets: PaginatedList<SuggestionTarget>) => map((targets: PaginatedList<SuggestionTarget>) =>
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) => { catchError((error: unknown) => {
if (error instanceof Error) { if (error instanceof Error) {
console.error(error.message); console.error(error.message);
} }
return of(new RetrieveAllTargetsErrorAction()); return of(new RetrieveTargetsBySourceErrorAction(action.payload.source));
}), }),
); );
}), }),
@@ -67,16 +72,27 @@ export class SuggestionTargetsEffects {
}), }),
), { dispatch: false }); ), { 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 * Fetch the current user suggestion
*/ */
refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe( refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe(
ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS),
switchMap(() => { switchMap(() => {
return this.store$.select((state: any) => state.core.auth.userId) return this.store$.select((state: any) => state.core.auth.user)
.pipe( .pipe(
switchMap((userId: string) => { switchMap((user: EPerson) => {
return this.suggestionsService.retrieveCurrentUserSuggestions(userId) return this.suggestionsService.retrieveCurrentUserSuggestions(user.uuid)
.pipe( .pipe(
map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)),
catchError((error: unknown) => { catchError((error: unknown) => {

View File

@@ -7,13 +7,21 @@ import {
/** /**
* The interface representing the OpenAIRE suggestion targets state. * The interface representing the OpenAIRE suggestion targets state.
*/ */
export interface SuggestionTargetState { export interface SuggestionTargetEntry {
targets: SuggestionTarget[]; targets: SuggestionTarget[];
processing: boolean; processing: boolean;
loaded: boolean; loaded: boolean;
totalPages: number; totalPages: number;
currentPage: number; currentPage: number;
totalElements: number; totalElements: number;
}
export interface SuggestionSourcesState {
[source: string]: SuggestionTargetEntry;
}
export interface SuggestionTargetState {
sources: SuggestionSourcesState;
currentUserTargets: SuggestionTarget[]; currentUserTargets: SuggestionTarget[];
currentUserTargetsVisited: boolean; currentUserTargetsVisited: boolean;
} }
@@ -21,13 +29,17 @@ export interface SuggestionTargetState {
/** /**
* Used for the OpenAIRE Suggestion Target state initialization. * Used for the OpenAIRE Suggestion Target state initialization.
*/ */
const SuggestionTargetInitialState: SuggestionTargetState = { const suggestionSourceTargetsInitialState: SuggestionTargetEntry = {
targets: [], targets: [],
processing: false, processing: false,
loaded: false, loaded: false,
totalPages: 0, totalPages: 0,
currentPage: 0, currentPage: 0,
totalElements: 0, totalElements: 0,
};
const SuggestionTargetInitialState: SuggestionTargetState = {
sources: {},
currentUserTargets: null, currentUserTargets: null,
currentUserTargetsVisited: false, currentUserTargetsVisited: false,
}; };
@@ -45,25 +57,42 @@ const SuggestionTargetInitialState: SuggestionTargetState = {
export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState {
switch (action.type) { switch (action.type) {
case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { 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: [], targets: [],
processing: true, processing: true,
}); });
return Object.assign({}, state, {
sources:
Object.assign({}, state.sources, {
[action.payload.source]: newSourceState,
}),
});
} }
case SuggestionTargetActionTypes.ADD_TARGETS: { case SuggestionTargetActionTypes.ADD_TARGETS: {
return Object.assign({}, state, { const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState);
targets: state.targets.concat(action.payload.targets), const newSourceState = Object.assign({}, sourceState, {
targets: sourceState.targets.concat(action.payload.targets),
processing: false, processing: false,
loaded: true, loaded: true,
totalPages: action.payload.totalPages, totalPages: action.payload.totalPages,
currentPage: state.currentPage, currentPage: action.payload.currentPage,
totalElements: action.payload.totalElements, totalElements: action.payload.totalElements,
}); });
return Object.assign({}, state, {
sources:
Object.assign({}, state.sources, {
[action.payload.source]: newSourceState,
}),
});
} }
case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { 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: [], targets: [],
processing: false, processing: false,
loaded: true, loaded: true,
@@ -71,6 +100,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a
currentPage: 0, currentPage: 0,
totalElements: 0, totalElements: 0,
}); });
return Object.assign({}, state, {
sources:
Object.assign({}, state.sources, {
[action.payload.source]: newSourceState,
}),
});
} }
case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: {
@@ -86,7 +122,8 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a
} }
case SuggestionTargetActionTypes.CLEAR_TARGETS: { case SuggestionTargetActionTypes.CLEAR_TARGETS: {
return Object.assign({}, state, { const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState);
const newSourceState = Object.assign({}, sourceState, {
targets: [], targets: [],
processing: false, processing: false,
loaded: false, loaded: false,
@@ -94,6 +131,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a
currentPage: 0, currentPage: 0,
totalElements: 0, totalElements: 0,
}); });
return Object.assign({}, state, {
sources:
Object.assign({}, state.sources, {
[action.payload.source]: newSourceState,
}),
});
} }
default: { default: {

View File

@@ -7,16 +7,16 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model'; import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionNotificationsState } from '../notifications.reducer';
import { import {
getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsSelector,
getCurrentUserSuggestionTargetsVisitedSelector, getCurrentUserSuggestionTargetsVisitedSelector,
getSuggestionTargetCurrentPageSelector, getSuggestionTargetCurrentPageSelector,
getSuggestionTargetTotalsSelector, getSuggestionTargetTotalsSelector,
isReciterSuggestionTargetProcessingSelector,
isSuggestionTargetLoadedSelector, isSuggestionTargetLoadedSelector,
isSuggestionTargetProcessingSelector,
suggestionTargetObjectSelector, suggestionTargetObjectSelector,
} from '../../suggestion-notifications/selectors'; } from './selectors';
import { SuggestionNotificationsState } from '../notifications.reducer';
import { import {
ClearSuggestionTargetsAction, ClearSuggestionTargetsAction,
MarkUserSuggestionsAsVisitedAction, MarkUserSuggestionsAsVisitedAction,
@@ -42,8 +42,8 @@ export class SuggestionTargetsStateService {
* @return Observable<SuggestionTarget> * @return Observable<SuggestionTarget>
* The list of Suggestion Targets. * The list of Suggestion Targets.
*/ */
public getSuggestionTargets(): Observable<SuggestionTarget[]> { public getSuggestionTargets(source: string): Observable<SuggestionTarget[]> {
return this.store.pipe(select(suggestionTargetObjectSelector())); return this.store.pipe(select(suggestionTargetObjectSelector(source)));
} }
/** /**
@@ -52,9 +52,9 @@ export class SuggestionTargetsStateService {
* @return Observable<boolean> * @return Observable<boolean>
* 'true' if the targets are loading, 'false' otherwise. * 'true' if the targets are loading, 'false' otherwise.
*/ */
public isSuggestionTargetsLoading(): Observable<boolean> { public isSuggestionTargetsLoading(source: string): Observable<boolean> {
return this.store.pipe( return this.store.pipe(
select(isSuggestionTargetLoadedSelector), select(isSuggestionTargetLoadedSelector(source)),
map((loaded: boolean) => !loaded), map((loaded: boolean) => !loaded),
); );
} }
@@ -65,8 +65,8 @@ export class SuggestionTargetsStateService {
* @return Observable<boolean> * @return Observable<boolean>
* 'true' if the targets are loaded, 'false' otherwise. * 'true' if the targets are loaded, 'false' otherwise.
*/ */
public isSuggestionTargetsLoaded(): Observable<boolean> { public isSuggestionTargetsLoaded(source: string): Observable<boolean> {
return this.store.pipe(select(isSuggestionTargetLoadedSelector)); return this.store.pipe(select(isSuggestionTargetLoadedSelector(source)));
} }
/** /**
@@ -75,8 +75,8 @@ export class SuggestionTargetsStateService {
* @return Observable<boolean> * @return Observable<boolean>
* 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
*/ */
public isSuggestionTargetsProcessing(): Observable<boolean> { public isSuggestionTargetsProcessing(source: string): Observable<boolean> {
return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector)); return this.store.pipe(select(isSuggestionTargetProcessingSelector(source)));
} }
/** /**
@@ -85,8 +85,8 @@ export class SuggestionTargetsStateService {
* @return Observable<number> * @return Observable<number>
* The number of the Suggestion Targets pages. * The number of the Suggestion Targets pages.
*/ */
public getSuggestionTargetsTotalPages(): Observable<number> { public getSuggestionTargetsTotalPages(source: string): Observable<number> {
return this.store.pipe(select(getSuggestionTargetTotalsSelector)); return this.store.pipe(select(getSuggestionTargetTotalsSelector(source)));
} }
/** /**
@@ -95,8 +95,8 @@ export class SuggestionTargetsStateService {
* @return Observable<number> * @return Observable<number>
* The number of the current Suggestion Targets page. * The number of the current Suggestion Targets page.
*/ */
public getSuggestionTargetsCurrentPage(): Observable<number> { public getSuggestionTargetsCurrentPage(source: string): Observable<number> {
return this.store.pipe(select(getSuggestionTargetCurrentPageSelector)); return this.store.pipe(select(getSuggestionTargetCurrentPageSelector(source)));
} }
/** /**
@@ -105,8 +105,8 @@ export class SuggestionTargetsStateService {
* @return Observable<number> * @return Observable<number>
* The number of the Suggestion Targets. * The number of the Suggestion Targets.
*/ */
public getSuggestionTargetsTotals(): Observable<number> { public getSuggestionTargetsTotals(source: string): Observable<number> {
return this.store.pipe(select(getSuggestionTargetTotalsSelector)); return this.store.pipe(select(getSuggestionTargetTotalsSelector(source)));
} }
/** /**
@@ -130,7 +130,7 @@ export class SuggestionTargetsStateService {
* The Suggestion Targets object. * The Suggestion Targets object.
*/ */
public getCurrentUserSuggestionTargets(): Observable<SuggestionTarget[]> { public getCurrentUserSuggestionTargets(): Observable<SuggestionTarget[]> {
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. * True if user already visited, false otherwise.
*/ */
public hasUserVisitedSuggestions(): Observable<boolean> { public hasUserVisitedSuggestions(): Observable<boolean> {
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. * Dispatch an action to clear the Reciter Suggestion Targets state.
*
* @param source
* the source of suggestion targets
*/ */
public dispatchClearSuggestionTargetsAction(): void { public dispatchClearSuggestionTargetsAction(source: string): void {
this.store.dispatch(new ClearSuggestionTargetsAction()); this.store.dispatch(new ClearSuggestionTargetsAction(source));
} }
/** /**

View File

@@ -44,8 +44,8 @@ export class SuggestionsNotificationComponent implements OnInit {
) { } ) { }
ngOnInit() { ngOnInit() {
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
this.suggestionsRD$ = this.suggestionTargetsStateService.getCurrentUserSuggestionTargets(); this.suggestionsRD$ = this.suggestionTargetsStateService.getCurrentUserSuggestionTargets();
this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction();
} }
/** /**

View File

@@ -16,6 +16,7 @@ import {
Observable, Observable,
of, of,
Subject, Subject,
Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
take, take,
@@ -53,7 +54,7 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy {
labelPrefix = 'notification.'; labelPrefix = 'notification.';
subscription; subscription: Subscription;
suggestionsRD$: Observable<SuggestionTarget[]>; suggestionsRD$: Observable<SuggestionTarget[]>;

View File

@@ -16,6 +16,7 @@ import { ResourceType } from '../core/shared/resource-type';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { mockSuggestionPublicationOne } from '../shared/mocks/publication-claim.mock'; import { mockSuggestionPublicationOne } from '../shared/mocks/publication-claim.mock';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { followLink } from '../shared/utils/follow-link-config.model';
import { SuggestionsService } from './suggestions.service'; import { SuggestionsService } from './suggestions.service';
describe('SuggestionsService test', () => { describe('SuggestionsService test', () => {
@@ -115,7 +116,7 @@ describe('SuggestionsService test', () => {
it('should retrieve current user suggestions', () => { it('should retrieve current user suggestions', () => {
service.retrieveCurrentUserSuggestions('1234'); service.retrieveCurrentUserSuggestions('1234');
expect(researcherProfileService.findById).toHaveBeenCalledWith('1234', true); expect(researcherProfileService.findById).toHaveBeenCalledWith('1234', true, true, followLink('item'));
}); });
it('should approve and import suggestion', () => { it('should approve and import suggestion', () => {

View File

@@ -29,7 +29,6 @@ import { ResearcherProfile } from '../core/profile/model/researcher-profile.mode
import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service'; import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service';
import { NoContent } from '../core/shared/NoContent.model'; import { NoContent } from '../core/shared/NoContent.model';
import { import {
getAllSucceededRemoteDataPayload,
getFinishedRemoteData, getFinishedRemoteData,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
@@ -42,6 +41,7 @@ import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
} from '../shared/empty.util'; } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths'; import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths';
/** /**
@@ -121,7 +121,7 @@ export class SuggestionsService {
* @return Observable<RemoteData<PaginatedList<Suggestion>>> * @return Observable<RemoteData<PaginatedList<Suggestion>>>
* The list of Suggestion. * The list of Suggestion.
*/ */
public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable<PaginatedList<Suggestion>> { public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable<RemoteData<PaginatedList<Suggestion>>> {
const [source, target] = targetId.split(':'); const [source, target] = targetId.split(':');
const findListOptions: FindListOptions = { const findListOptions: FindListOptions = {
@@ -130,9 +130,7 @@ export class SuggestionsService {
sort: sortOptions, sort: sortOptions,
}; };
return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions).pipe( return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions);
getAllSucceededRemoteDataPayload(),
);
} }
/** /**
@@ -169,7 +167,7 @@ export class SuggestionsService {
if (hasNoValue(userUuid)) { if (hasNoValue(userUuid)) {
return of([]); return of([]);
} }
return this.researcherProfileService.findById(userUuid, true).pipe( return this.researcherProfileService.findById(userUuid, true, true, followLink('item')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
mergeMap((profile: RemoteData<ResearcherProfile> ) => { mergeMap((profile: RemoteData<ResearcherProfile> ) => {
if (isNotEmpty(profile) && profile.hasSucceeded && isNotEmpty(profile.payload)) { if (isNotEmpty(profile) && profile.hasSucceeded && isNotEmpty(profile.payload)) {

View File

@@ -1 +1 @@
<ds-publication-claim [source]="'oaire'"></ds-publication-claim> <ds-publication-claim [source]="'openaire'"></ds-publication-claim>

View File

@@ -2,7 +2,9 @@ import { of as observableOf } from 'rxjs';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { SearchResult } from '../search/models/search-result.model'; import { SearchResult } from '../search/models/search-result.model';
import { createPaginatedList } from '../testing/utils.test';
// REST Mock --------------------------------------------------------------------- // REST Mock ---------------------------------------------------------------------
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
@@ -1345,7 +1347,7 @@ export function getMockSuggestionNotificationsStateService(): any {
export function getMockSuggestionsService(): any { export function getMockSuggestionsService(): any {
return jasmine.createSpyObj('SuggestionsService', { return jasmine.createSpyObj('SuggestionsService', {
getTargets: jasmine.createSpy('getTargets'), getTargets: jasmine.createSpy('getTargets'),
getSuggestions: observableOf([]), getSuggestions: createSuccessfulRemoteDataObject$(createPaginatedList([])),
clearSuggestionRequests: jasmine.createSpy('clearSuggestionRequests'), clearSuggestionRequests: jasmine.createSpy('clearSuggestionRequests'),
deleteReviewedSuggestion: jasmine.createSpy('deleteReviewedSuggestion'), deleteReviewedSuggestion: jasmine.createSpy('deleteReviewedSuggestion'),
retrieveCurrentUserSuggestions: jasmine.createSpy('retrieveCurrentUserSuggestions'), retrieveCurrentUserSuggestions: jasmine.createSpy('retrieveCurrentUserSuggestions'),

View File

@@ -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<SuggestionNotificationsState>('suggestionNotifications');
// Reciter Suggestion Targets
// ----------------------------------------------------------------------------
/**
* Returns the Suggestion Targets State.
* @function suggestionTargetStateSelector
* @return {SuggestionNotificationsState}
*/
export function suggestionTargetStateSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTargetState> {
return subStateSelector<SuggestionNotificationsState, SuggestionTargetState>(suggestionNotificationsSelector, 'suggestionTarget');
}
/**
* Returns the Suggestion Targets list.
* @function suggestionTargetObjectSelector
* @return {SuggestionTarget[]}
*/
export function suggestionTargetObjectSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTarget[]> {
return subStateSelector<SuggestionNotificationsState, SuggestionTarget[]>(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,
);

View File

@@ -2,7 +2,8 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<ng-container *ngVar="(suggestionsRD$ | async) as suggestionsRD"> <ng-container *ngVar="(suggestionsRD$ | async) as suggestionsRD">
<div *ngIf="suggestionsRD?.pageInfo?.totalElements > 0"> <ds-loading *ngIf="(processing$ | async)"></ds-loading>
<div *ngIf="(processing$ | async) !== true && suggestionsRD?.pageInfo?.totalElements > 0">
<h1> <h1>
{{'suggestion.suggestionFor' | translate}} {{'suggestion.suggestionFor' | translate}}
@@ -21,7 +22,6 @@
(ignoreSuggestionClicked)="ignoreSuggestionAllSelected()"></ds-suggestion-actions> (ignoreSuggestionClicked)="ignoreSuggestionAllSelected()"></ds-suggestion-actions>
<i class='fas fa-circle-notch fa-spin' *ngIf="isBulkOperationPending"></i> <i class='fas fa-circle-notch fa-spin' *ngIf="isBulkOperationPending"></i>
</div> </div>
<ds-loading *ngIf="(processing$ | async)"></ds-loading>
<ds-pagination *ngIf="(processing$ | async) !== true" <ds-pagination *ngIf="(processing$ | async) !== true"
[paginationOptions]="paginationOptions" [paginationOptions]="paginationOptions"
[sortOptions]="paginationSortConfig" [sortOptions]="paginationSortConfig"
@@ -41,7 +41,9 @@
</ul> </ul>
</ds-pagination> </ds-pagination>
</div> </div>
<div *ngIf="suggestionsRD?.pageInfo?.totalElements === 0">{{ 'suggestion.count.missing' | translate }}</div> <ds-alert *ngIf="(processing$ | async) !== true && (suggestionsRD?.pageInfo?.totalElements === 0 || !suggestionsRD)" [type]="'alert-info'">
{{'suggestion.count.missing' | translate}}
</ds-alert>
</ng-container> </ng-container>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
async,
ComponentFixture, ComponentFixture,
fakeAsync, fakeAsync,
TestBed, TestBed,
tick, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { import {
@@ -70,7 +69,7 @@ describe('SuggestionPageComponent', () => {
}); });
const paginationService = new PaginationServiceStub(); const paginationService = new PaginationServiceStub();
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -106,7 +105,7 @@ describe('SuggestionPageComponent', () => {
}); });
it('should create', () => { it('should create', () => {
spyOn(component, 'updatePage').and.stub(); spyOn(component, 'updatePage').and.callThrough();
scheduler.schedule(() => fixture.detectChanges()); scheduler.schedule(() => fixture.detectChanges());
scheduler.flush(); scheduler.flush();
@@ -118,70 +117,72 @@ describe('SuggestionPageComponent', () => {
}); });
it('should update page on pagination change', () => { 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(); scheduler.flush();
component.onPaginationChange();
expect(component.updatePage).toHaveBeenCalled(); 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.processing$, 'next');
spyOn(component.suggestionsRD$, 'next'); spyOn(component.suggestionsRD$, 'next');
scheduler.schedule(() => fixture.detectChanges()); component.targetId$ = observableOf('testid');
scheduler.schedule(() => component.updatePage().subscribe());
scheduler.flush(); scheduler.flush();
paginationService.getFindListOptions().subscribe(() => {
expect(component.processing$.next).toHaveBeenCalled(); expect(component.processing$.next).toHaveBeenCalledTimes(2);
expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled();
expect(component.suggestionsRD$.next).toHaveBeenCalled(); expect(component.suggestionsRD$.next).toHaveBeenCalled();
expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled();
done();
});
component.updatePage();
}); });
it('should flag suggestion for deletion', fakeAsync(() => { 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(); scheduler.flush();
component.ignoreSuggestion('1');
expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1');
expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled();
tick(201);
expect(component.updatePage).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled();
})); }));
it('should flag all suggestion for deletion', () => { 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(); scheduler.flush();
component.ignoreSuggestionAllSelected();
expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled();
expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled();
expect(component.updatePage).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled();
}); });
it('should approve and import', () => { 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(); scheduler.flush();
component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport);
expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled();
expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled();
expect(component.updatePage).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled();
}); });
it('should approve and import multiple suggestions', () => { 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(); scheduler.flush();
component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport);
expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled();
expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled();
expect(component.updatePage).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled();

View File

@@ -26,7 +26,7 @@ import {
distinctUntilChanged, distinctUntilChanged,
map, map,
switchMap, switchMap,
take, tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AuthService } from '../core/auth/auth.service'; 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 { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model';
import { PaginationService } from '../core/pagination/pagination.service'; import { PaginationService } from '../core/pagination/pagination.service';
import { redirectOn4xx } from '../core/shared/authorized.operators'; 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 { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component'; import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component';
@@ -52,6 +55,7 @@ import {
SuggestionBulkResult, SuggestionBulkResult,
SuggestionsService, SuggestionsService,
} from '../notifications/suggestions.service'; } from '../notifications/suggestions.service';
import { AlertComponent } from '../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { PaginationComponent } from '../shared/pagination/pagination.component'; import { PaginationComponent } from '../shared/pagination/pagination.component';
@@ -74,6 +78,7 @@ import { getWorkspaceItemEditRoute } from '../workflowitems-edit-page/workflowit
PaginationComponent, PaginationComponent,
SuggestionListElementComponent, SuggestionListElementComponent,
NgForOf, NgForOf,
AlertComponent,
], ],
standalone: true, standalone: true,
}) })
@@ -149,14 +154,15 @@ export class SuggestionsPageComponent implements OnInit {
); );
this.targetRD$.pipe( this.targetRD$.pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
).subscribe((suggestionTarget: SuggestionTarget) => { tap((suggestionTarget: SuggestionTarget) => {
this.suggestionTarget = suggestionTarget; this.suggestionTarget = suggestionTarget;
this.suggestionId = suggestionTarget.id; this.suggestionId = suggestionTarget.id;
this.researcherName = suggestionTarget.display; this.researcherName = suggestionTarget.display;
this.suggestionSource = suggestionTarget.source; this.suggestionSource = suggestionTarget.source;
this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget);
this.updatePage(); }),
}); switchMap(() => this.updatePage()),
).subscribe();
this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction();
} }
@@ -165,13 +171,13 @@ export class SuggestionsPageComponent implements OnInit {
* Called when one of the pagination settings is changed * Called when one of the pagination settings is changed
*/ */
onPaginationChange() { onPaginationChange() {
this.updatePage(); this.updatePage().subscribe();
} }
/** /**
* Update the list of suggestions * Update the list of suggestions
*/ */
updatePage() { updatePage(): Observable<RemoteData<PaginatedList<Suggestion>>> {
this.processing$.next(true); this.processing$.next(true);
const pageConfig$: Observable<FindListOptions> = this.paginationService.getFindListOptions( const pageConfig$: Observable<FindListOptions> = this.paginationService.getFindListOptions(
this.paginationOptions.id, this.paginationOptions.id,
@@ -179,7 +185,8 @@ export class SuggestionsPageComponent implements OnInit {
).pipe( ).pipe(
distinctUntilChanged(), distinctUntilChanged(),
); );
combineLatest([this.targetId$, pageConfig$]).pipe(
return combineLatest([this.targetId$, pageConfig$]).pipe(
switchMap(([targetId, config]: [string, FindListOptions]) => { switchMap(([targetId, config]: [string, FindListOptions]) => {
return this.suggestionService.getSuggestions( return this.suggestionService.getSuggestions(
targetId, targetId,
@@ -188,12 +195,18 @@ export class SuggestionsPageComponent implements OnInit {
config.sort, config.sort,
); );
}), }),
take(1), getFirstCompletedRemoteData(),
).subscribe((results: PaginatedList<Suggestion>) => { tap((resultsRD: RemoteData<PaginatedList<Suggestion>>) => {
this.processing$.next(false); this.processing$.next(false);
this.suggestionsRD$.next(results); if (resultsRD.hasSucceeded) {
this.suggestionService.clearSuggestionRequests(); this.suggestionsRD$.next(resultsRD.payload);
}); } else {
this.suggestionsRD$.next(null);
}
this.suggestionService.clearSuggestionRequests();
}),
);
} }
/** /**
@@ -201,11 +214,10 @@ export class SuggestionsPageComponent implements OnInit {
* @suggestionId * @suggestionId
*/ */
ignoreSuggestion(suggestionId) { ignoreSuggestion(suggestionId) {
this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { this.suggestionService.ignoreSuggestion(suggestionId).pipe(
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); tap(() => this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction()),
//We add a little delay in the page refresh so that we ensure the deletion has been propagated switchMap(() => this.updatePage()),
setTimeout(() => this.updatePage(), 200); ).subscribe();
});
} }
/** /**
@@ -213,11 +225,9 @@ export class SuggestionsPageComponent implements OnInit {
*/ */
ignoreSuggestionAllSelected() { ignoreSuggestionAllSelected() {
this.isBulkOperationPending = true; this.isBulkOperationPending = true;
this.suggestionService this.suggestionService.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)).pipe(
.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) tap((results: SuggestionBulkResult) => {
.subscribe((results: SuggestionBulkResult) => {
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
this.updatePage();
this.isBulkOperationPending = false; this.isBulkOperationPending = false;
this.selectedSuggestions = {}; this.selectedSuggestions = {};
if (results.success > 0) { if (results.success > 0) {
@@ -230,7 +240,9 @@ export class SuggestionsPageComponent implements OnInit {
this.translateService.get('suggestion.ignoreSuggestion.bulk.error', this.translateService.get('suggestion.ignoreSuggestion.bulk.error',
{ count: results.fails })); { 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 * @param event contains the suggestion and the target collection
*/ */
approveAndImport(event: SuggestionApproveAndImport) { approveAndImport(event: SuggestionApproveAndImport) {
this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId).pipe(
.subscribe((workspaceitem: WorkspaceItem) => { tap((workspaceitem: WorkspaceItem) => {
const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) });
this.notificationService.success('', content, { timeOut:0 }, true); this.notificationService.success('', content, { timeOut:0 }, true);
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
this.updatePage(); }),
}); switchMap(() => this.updatePage()),
).subscribe();
} }
/** /**
@@ -253,11 +266,9 @@ export class SuggestionsPageComponent implements OnInit {
*/ */
approveAndImportAllSelected(event: SuggestionApproveAndImport) { approveAndImportAllSelected(event: SuggestionApproveAndImport) {
this.isBulkOperationPending = true; this.isBulkOperationPending = true;
this.suggestionService this.suggestionService.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId).pipe(
.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) tap((results: SuggestionBulkResult) => {
.subscribe((results: SuggestionBulkResult) => {
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
this.updatePage();
this.isBulkOperationPending = false; this.isBulkOperationPending = false;
this.selectedSuggestions = {}; this.selectedSuggestions = {};
if (results.success > 0) { if (results.success > 0) {
@@ -270,7 +281,9 @@ export class SuggestionsPageComponent implements OnInit {
this.translateService.get('suggestion.approveAndImport.bulk.error', this.translateService.get('suggestion.approveAndImport.bulk.error',
{ count: results.fails })); { count: results.fails }));
} }
}); }),
switchMap(() => this.updatePage()),
).subscribe();
} }
/** /**

View File

@@ -5,12 +5,11 @@ import {
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find } from 'rxjs/operators';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service'; 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 * Method for resolving a suggestion target based on the parameters in the current route
@@ -26,6 +25,6 @@ export const suggestionsPageResolver: ResolveFn<RemoteData<SuggestionTarget>> =
suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService),
): Observable<RemoteData<SuggestionTarget>> => { ): Observable<RemoteData<SuggestionTarget>> => {
return suggestionsDataService.getTargetById(route.params.targetId).pipe( return suggestionsDataService.getTargetById(route.params.targetId).pipe(
find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), getFirstCompletedRemoteData(),
); );
}; };