Returns RemoteData when retrieving a submission and handles error response

This commit is contained in:
Giuseppe Digilio
2019-01-23 13:10:19 +01:00
parent 4505f6e525
commit 491a8f7d2b
8 changed files with 128 additions and 81 deletions

View File

@@ -5,7 +5,6 @@ import { RestRequest } from '../data/request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response-cache.models';
import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util'; import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util';
import { ConfigObject } from '../config/models/config.model'; import { ConfigObject } from '../config/models/config.model';
import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
@@ -18,9 +17,6 @@ import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model
import { normalizeSectionData } from './models/workspaceitem-sections.model'; import { normalizeSectionData } from './models/workspaceitem-sections.model';
import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
import { NormalizedEditItem } from './models/normalized-edititem.model'; import { NormalizedEditItem } from './models/normalized-edititem.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedSubmissionObject } from './models/normalized-submission-object.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
@Injectable() @Injectable()
export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
@@ -57,7 +53,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
const normalizedDefinition = Array.of(); const normalizedDefinition = Array.of();
const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition); const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition);
processedList.forEach((item, index) => { processedList.forEach((item) => {
let normalizedItem = Object.assign({}, item); let normalizedItem = Object.assign({}, item);
// In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form // In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form
@@ -92,10 +88,10 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
precessedSection[sectionId] = normalizedSectionData; precessedSection[sectionId] = normalizedSectionData;
} }
}); });
normalizedItem = Object.assign({}, item, {sections: precessedSection}); normalizedItem = Object.assign({}, item, { sections: precessedSection });
} }
} }
normalizedDefinition.push( normalizedItem); normalizedDefinition.push(normalizedItem);
}); });
return normalizedDefinition; return normalizedDefinition;

View File

@@ -16,6 +16,7 @@ import { getMockTranslateService } from '../../shared/mocks/mock-translate.servi
import { RouterStub } from '../../shared/testing/router-stub'; import { RouterStub } from '../../shared/testing/router-stub';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { mockSubmissionObject } from '../../shared/mocks/mock-submission'; import { mockSubmissionObject } from '../../shared/mocks/mock-submission';
import { RemoteData } from '../../core/data/remote-data';
describe('SubmissionEditComponent Component', () => { describe('SubmissionEditComponent Component', () => {
@@ -65,7 +66,14 @@ describe('SubmissionEditComponent Component', () => {
it('should init properly when a valid SubmissionObject has been retrieved', fakeAsync(() => { it('should init properly when a valid SubmissionObject has been retrieved', fakeAsync(() => {
route.testParams = { id: submissionId }; route.testParams = { id: submissionId };
submissionServiceStub.retrieveSubmission.and.returnValue(observableOf(submissionObject)); submissionServiceStub.retrieveSubmission.and.returnValue(observableOf(
new RemoteData(
false,
false,
true,
null,
submissionObject)
));
fixture.detectChanges(); fixture.detectChanges();
@@ -80,7 +88,14 @@ describe('SubmissionEditComponent Component', () => {
it('should redirect to mydspace when an empty SubmissionObject has been retrieved', fakeAsync(() => { it('should redirect to mydspace when an empty SubmissionObject has been retrieved', fakeAsync(() => {
route.testParams = { id: submissionId }; route.testParams = { id: submissionId };
submissionServiceStub.retrieveSubmission.and.returnValue(observableOf({})); submissionServiceStub.retrieveSubmission.and.returnValue(observableOf(
new RemoteData(
false,
false,
true,
null,
{})
));
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { flatMap, tap } from 'rxjs/operators'; import { filter, switchMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
@@ -12,6 +12,7 @@ import { SubmissionService } from '../submission.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../core/submission/models/submission-object.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data';
@Component({ @Component({
selector: 'ds-submission-edit', selector: 'ds-submission-edit',
@@ -42,21 +43,28 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
ngOnInit() { ngOnInit() {
this.subs.push(this.route.paramMap.pipe( this.subs.push(this.route.paramMap.pipe(
flatMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))) switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))),
).subscribe((submissionObject: SubmissionObject) => { // NOTE new submission is retrieved on the browser side only, so get null on server side rendering
// NOTE new submission is retrieved on the browser side only filter((submissionObjectRD: RemoteData<SubmissionObject>) => isNotNull(submissionObjectRD))
if (isNotNull(submissionObject)) { ).subscribe((submissionObjectRD: RemoteData<SubmissionObject>) => {
if (isEmpty(submissionObject)) { if (submissionObjectRD.hasSucceeded) {
if (isEmpty(submissionObjectRD.payload)) {
this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit'));
this.router.navigate(['/mydspace']); this.router.navigate(['/mydspace']);
} else { } else {
this.submissionId = submissionObject.id; this.submissionId = submissionObjectRD.payload.id.toString();
this.collectionId = (submissionObject.collection as Collection).id; this.collectionId = (submissionObjectRD.payload.collection as Collection).id;
this.selfUrl = submissionObject.self; this.selfUrl = submissionObjectRD.payload.self;
this.sections = submissionObject.sections; this.sections = submissionObjectRD.payload.sections;
this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel);
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
} }
} else {
if (submissionObjectRD.error.statusCode === 404) {
// redirect to not found page
this.router.navigate(['/404'], { skipLocationChange: true });
}
// TODO handle generic error
} }
})); }));
} }

View File

@@ -1,38 +1,38 @@
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
import { findKey, uniqWith, isEqual, differenceWith } from 'lodash'; import { differenceWith, findKey, isEqual, uniqWith } from 'lodash';
import { import {
ChangeSubmissionCollectionAction,
CompleteInitSubmissionFormAction, CompleteInitSubmissionFormAction,
DeleteSectionErrorsAction,
DeleteUploadedFileAction, DeleteUploadedFileAction,
DepositSubmissionAction,
DepositSubmissionErrorAction,
DepositSubmissionSuccessAction,
DisableSectionAction, DisableSectionAction,
EditFileDataAction, EditFileDataAction,
EnableSectionAction, EnableSectionAction,
NewUploadedFileAction, InertSectionErrorsAction,
InitSectionAction,
InitSubmissionFormAction, InitSubmissionFormAction,
NewUploadedFileAction,
RemoveSectionErrorsAction,
ResetSubmissionFormAction,
SaveAndDepositSubmissionAction,
SaveForLaterSubmissionFormAction,
SaveForLaterSubmissionFormErrorAction,
SaveForLaterSubmissionFormSuccessAction,
SaveSubmissionFormAction,
SaveSubmissionFormErrorAction,
SaveSubmissionFormSuccessAction,
SaveSubmissionSectionFormAction,
SaveSubmissionSectionFormErrorAction,
SaveSubmissionSectionFormSuccessAction,
SectionStatusChangeAction, SectionStatusChangeAction,
SetActiveSectionAction,
SubmissionObjectAction, SubmissionObjectAction,
SubmissionObjectActionTypes, SubmissionObjectActionTypes,
InertSectionErrorsAction, UpdateSectionDataAction
DeleteSectionErrorsAction,
ResetSubmissionFormAction,
UpdateSectionDataAction,
SaveSubmissionFormAction,
SetActiveSectionAction,
SaveSubmissionSectionFormAction,
DepositSubmissionAction,
DepositSubmissionSuccessAction,
DepositSubmissionErrorAction,
ChangeSubmissionCollectionAction,
SaveSubmissionFormSuccessAction,
SaveSubmissionFormErrorAction,
SaveSubmissionSectionFormSuccessAction,
SaveSubmissionSectionFormErrorAction,
InitSectionAction,
RemoveSectionErrorsAction,
SaveForLaterSubmissionFormAction,
SaveAndDepositSubmissionAction,
SaveForLaterSubmissionFormSuccessAction,
SaveForLaterSubmissionFormErrorAction
} from './submission-objects.actions'; } from './submission-objects.actions';
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model';
@@ -117,7 +117,6 @@ export function submissionObjectReducer(state = initialState, action: Submission
} }
case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS: case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS:
case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS:
case SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS: case SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS:
case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR: case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR:
case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR: case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR:

View File

@@ -4,6 +4,7 @@ import { Observable, of as observableOf } from 'rxjs';
import { SubmissionService } from './submission.service'; import { SubmissionService } from './submission.service';
import { SubmissionObject } from '../core/submission/models/submission-object.model'; import { SubmissionObject } from '../core/submission/models/submission-object.model';
import { RemoteData } from '../core/data/remote-data';
@Injectable() @Injectable()
export class ServerSubmissionService extends SubmissionService { export class ServerSubmissionService extends SubmissionService {
@@ -12,7 +13,7 @@ export class ServerSubmissionService extends SubmissionService {
return observableOf(null); return observableOf(null);
} }
retrieveSubmission(submissionId): Observable<SubmissionObject> { retrieveSubmission(submissionId): Observable<RemoteData<SubmissionObject>> {
return observableOf(null); return observableOf(null);
} }

View File

@@ -6,7 +6,7 @@ import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/
import { ResponseCacheService } from '../core/cache/response-cache.service'; import { ResponseCacheService } from '../core/cache/response-cache.service';
import { RequestService } from '../core/data/request.service'; import { RequestService } from '../core/data/request.service';
import { ResponseCacheEntry } from '../core/cache/response-cache.reducer'; import { ResponseCacheEntry } from '../core/cache/response-cache.reducer';
import { SubmissionSuccessResponse } from '../core/cache/response-cache.models'; import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../core/cache/response-cache.models';
import { isNotEmpty } from '../shared/empty.util'; import { isNotEmpty } from '../shared/empty.util';
import { import {
DeleteRequest, DeleteRequest,
@@ -36,25 +36,15 @@ export class SubmissionRestService {
protected submitData(request: RestRequest): Observable<SubmitDataResponseDefinitionObject> { protected submitData(request: RestRequest): Observable<SubmitDataResponseDefinitionObject> {
const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response)); const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response));
const errorResponses = responses.pipe( const errorResponses = responses.pipe(
filter((response: SubmissionSuccessResponse) => !response.isSuccessful), filter((response: RestResponse) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`Couldn't send data to server`))) mergeMap((error: ErrorResponse) => observableThrowError(error))
); );
const successResponses = responses.pipe( const successResponses = responses.pipe(
filter((response: SubmissionSuccessResponse) => response.isSuccessful), filter((response: RestResponse) => response.isSuccessful),
map((response: SubmissionSuccessResponse) => response.dataDefinition as any), map((response: SubmissionSuccessResponse) => response.dataDefinition as any),
distinctUntilChanged() distinctUntilChanged()
); );
return observableMerge(errorResponses, successResponses); return observableMerge(errorResponses, successResponses);
/* const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
observableThrowError(new Error(`Couldn't send data to server`))),
successResponse
.filter((response: SubmissionSuccessResponse) => isNotEmpty(response))
.map((response: SubmissionSuccessResponse) => response.dataDefinition)
.distinctUntilChanged());*/
} }
protected fetchRequest(request: RestRequest): Observable<SubmitDataResponseDefinitionObject> { protected fetchRequest(request: RestRequest): Observable<SubmitDataResponseDefinitionObject> {
@@ -62,8 +52,8 @@ export class SubmissionRestService {
map((entry: ResponseCacheEntry) => entry.response), map((entry: ResponseCacheEntry) => entry.response),
tap(() => this.responseCache.remove(request.href))); tap(() => this.responseCache.remove(request.href)));
const errorResponses = responses.pipe( const errorResponses = responses.pipe(
filter((response: SubmissionSuccessResponse) => !response.isSuccessful), filter((response: RestResponse) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the data`))) mergeMap((error: ErrorResponse) => observableThrowError(error))
); );
const successResponses = responses.pipe( const successResponses = responses.pipe(
filter((response: SubmissionSuccessResponse) => response.isSuccessful && isNotEmpty(response)), filter((response: SubmissionSuccessResponse) => response.isSuccessful && isNotEmpty(response)),
@@ -71,18 +61,6 @@ export class SubmissionRestService {
distinctUntilChanged() distinctUntilChanged()
); );
return observableMerge(errorResponses, successResponses); return observableMerge(errorResponses, successResponses);
/* const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.do(() => this.responseCache.remove(request.href))
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
observableThrowError(new Error(`Couldn't retrieve the data`))),
successResponse
.filter((response: SubmissionSuccessResponse) => isNotEmpty(response))
.map((response: SubmissionSuccessResponse) => response.dataDefinition)
.distinctUntilChanged());*/
} }
protected getEndpointByIDHref(endpoint, resourceID): string { protected getEndpointByIDHref(endpoint, resourceID): string {

View File

@@ -30,9 +30,13 @@ import {
ResetSubmissionFormAction, ResetSubmissionFormAction,
SaveAndDepositSubmissionAction, SaveAndDepositSubmissionAction,
SaveForLaterSubmissionFormAction, SaveForLaterSubmissionFormAction,
SaveSubmissionFormAction, SaveSubmissionSectionFormAction, SaveSubmissionFormAction,
SaveSubmissionSectionFormAction,
SetActiveSectionAction SetActiveSectionAction
} from './objects/submission-objects.actions'; } from './objects/submission-objects.actions';
import { RemoteData } from '../core/data/remote-data';
import { RemoteDataError } from '../core/data/remote-data-error';
import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError';
describe('SubmissionService test suite', () => { describe('SubmissionService test suite', () => {
const config = MOCK_SUBMISSION_CONFIG; const config = MOCK_SUBMISSION_CONFIG;
@@ -363,7 +367,7 @@ describe('SubmissionService test suite', () => {
beforeEach(() => { beforeEach(() => {
service = TestBed.get(SubmissionService); service = TestBed.get(SubmissionService);
spyOn((service as any).store, 'dispatch').and.callThrough() spyOn((service as any).store, 'dispatch').and.callThrough();
}); });
describe('changeSubmissionCollection', () => { describe('changeSubmissionCollection', () => {
@@ -840,11 +844,36 @@ describe('SubmissionService test suite', () => {
const result = service.retrieveSubmission('826'); const result = service.retrieveSubmission('826');
const expected = cold('(b|)', { const expected = cold('(b|)', {
b: mockSubmissionRestResponse[0] b: new RemoteData(
false,
false,
true,
null,
mockSubmissionRestResponse[0])
}); });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should catch error from REST endpoint', () => {
(service as any).restService.getDataById.and.callFake(
() => observableThrowError({
statusCode: 500,
statusText: 'Internal Server Error',
errorMessage: 'Error message'
})
);
const result = service.retrieveSubmission('826').subscribe((r) => {
expect(r).toEqual(new RemoteData(
false,
false,
false,
new RemoteDataError(500, 'Internal Server Error', 'Error message'),
null
))
});
});
}); });
describe('setActiveSection', () => { describe('setActiveSection', () => {

View File

@@ -17,7 +17,8 @@ import {
ResetSubmissionFormAction, ResetSubmissionFormAction,
SaveAndDepositSubmissionAction, SaveAndDepositSubmissionAction,
SaveForLaterSubmissionFormAction, SaveForLaterSubmissionFormAction,
SaveSubmissionFormAction, SaveSubmissionSectionFormAction, SaveSubmissionFormAction,
SaveSubmissionSectionFormAction,
SetActiveSectionAction SetActiveSectionAction
} from './objects/submission-objects.actions'; } from './objects/submission-objects.actions';
import { import {
@@ -39,6 +40,9 @@ import { SectionsType } from './sections/sections-type';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { SubmissionDefinitionsModel } from '../core/config/models/config-submission-definitions.model'; import { SubmissionDefinitionsModel } from '../core/config/models/config-submission-definitions.model';
import { WorkspaceitemSectionsObject } from '../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionsObject } from '../core/submission/models/workspaceitem-sections.model';
import { RemoteData } from '../core/data/remote-data';
import { ErrorResponse } from '../core/cache/response-cache.models';
import { RemoteDataError } from '../core/data/remote-data-error';
@Injectable() @Injectable()
export class SubmissionService { export class SubmissionService {
@@ -244,7 +248,7 @@ export class SubmissionService {
} }
notifyNewSection(submissionId: string, sectionId: string, sectionType?: SectionsType) { notifyNewSection(submissionId: string, sectionId: string, sectionType?: SectionsType) {
this.translate.get('submission.sections.general.metadata-extracted-new-section', {sectionId}).pipe( this.translate.get('submission.sections.general.metadata-extracted-new-section', { sectionId }).pipe(
first()) first())
.subscribe((m) => { .subscribe((m) => {
this.notificationsService.info(null, m, null, true); this.notificationsService.info(null, m, null, true);
@@ -252,7 +256,9 @@ export class SubmissionService {
} }
redirectToMyDSpace() { redirectToMyDSpace() {
this.routeService.getPreviousUrl().subscribe((previousUrl: string) => { this.routeService.getPreviousUrl().pipe(
first()
).subscribe((previousUrl: string) => {
if (isEmpty(previousUrl) || !previousUrl.startsWith('/mydspace')) { if (isEmpty(previousUrl) || !previousUrl.startsWith('/mydspace')) {
this.router.navigate(['/mydspace']); this.router.navigate(['/mydspace']);
} else { } else {
@@ -276,11 +282,26 @@ export class SubmissionService {
this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition)); this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition));
} }
retrieveSubmission(submissionId): Observable<SubmissionObject> { retrieveSubmission(submissionId): Observable<RemoteData<SubmissionObject>> {
return this.restService.getDataById(this.getSubmissionObjectLinkName(), submissionId).pipe( return this.restService.getDataById(this.getSubmissionObjectLinkName(), submissionId).pipe(
filter((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)), filter((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)),
first(), first(),
map((submissionObjects: SubmissionObject[]) => submissionObjects[0])); map((submissionObjects: SubmissionObject[]) => new RemoteData(
false,
false,
true,
null,
submissionObjects[0])),
catchError((errorResponse: ErrorResponse) => {
return observableOf(new RemoteData(
false,
false,
false,
new RemoteDataError(errorResponse.statusCode, errorResponse.statusText, errorResponse.errorMessage),
null
))
})
);
} }
setActiveSection(submissionId, sectionId) { setActiveSection(submissionId, sectionId) {