diff --git a/src/app/core/submission/submission-rest.service.spec.ts b/src/app/core/submission/submission-rest.service.spec.ts index ee6a157b82..c4334c36b3 100644 --- a/src/app/core/submission/submission-rest.service.spec.ts +++ b/src/app/core/submission/submission-rest.service.spec.ts @@ -1,4 +1,5 @@ import { getTestScheduler } from 'jasmine-marbles'; +import { of } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; @@ -13,6 +14,7 @@ import { SubmissionRequest, } from '../data/request.models'; import { RequestService } from '../data/request.service'; +import { RequestEntry } from '../data/request-entry.model'; import { SubmissionRestService } from './submission-rest.service'; describe('SubmissionRestService test suite', () => { @@ -38,7 +40,9 @@ describe('SubmissionRestService test suite', () => { } beforeEach(() => { - requestService = getMockRequestService(); + requestService = getMockRequestService(of(Object.assign(new RequestEntry(), { + request: new SubmissionRequest('mock-request-uuid', 'mock-request-href'), + }))); rdbService = getMockRemoteDataBuildService(); scheduler = getTestScheduler(); halService = new HALEndpointServiceStub(resourceEndpointURL); @@ -62,7 +66,7 @@ describe('SubmissionRestService test suite', () => { scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe()); scheduler.flush(); - expect(requestService.send).toHaveBeenCalledWith(expected); + expect(requestService.send).toHaveBeenCalledWith(expected, false); }); }); diff --git a/src/app/core/submission/submission-rest.service.ts b/src/app/core/submission/submission-rest.service.ts index b1f4ad9e1b..cd4b3be955 100644 --- a/src/app/core/submission/submission-rest.service.ts +++ b/src/app/core/submission/submission-rest.service.ts @@ -1,15 +1,20 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { + Observable, + skipWhile, +} from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, + switchMap, tap, } from 'rxjs/operators'; import { hasValue, + hasValueOperator, isNotEmpty, } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -25,7 +30,6 @@ import { } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { RequestError } from '../data/request-error.model'; -import { RestRequest } from '../data/rest-request.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; @@ -33,6 +37,23 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon import { URLCombiner } from '../url-combiner/url-combiner'; import { SubmissionResponse } from './submission-response.model'; +/** + * Retrieve the first emitting payload's dataDefinition, or throw an error if the request failed + */ +export const getFirstDataDefinition = () => + (source: Observable>): Observable => + source.pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + if (response.hasFailed) { + throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError); + } else { + return hasValue(response?.payload?.dataDefinition) ? response.payload.dataDefinition : [response.payload]; + } + }), + distinctUntilChanged(), + ); + /** * The service handling all submission REST requests */ @@ -56,15 +77,7 @@ export class SubmissionRestService { */ protected fetchRequest(requestId: string): Observable { return this.rdbService.buildFromRequestUUID(requestId).pipe( - getFirstCompletedRemoteData(), - map((response: RemoteData) => { - if (response.hasFailed) { - throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError); - } else { - return hasValue(response.payload) ? response.payload.dataDefinition : response.payload; - } - }), - distinctUntilChanged(), + getFirstDataDefinition(), ); } @@ -116,21 +129,52 @@ export class SubmissionRestService { * The endpoint link name * @param id * The submission Object to retrieve + * @param useCachedVersionIfAvailable + * If this is true, the request will only be sent if there's no valid & cached version. Defaults to false * @return Observable * server response */ - public getDataById(linkName: string, id: string): Observable { - const requestId = this.requestService.generateRequestId(); + public getDataById(linkName: string, id: string, useCachedVersionIfAvailable = false): Observable { return this.halService.getEndpoint(linkName).pipe( map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)), filter((href: string) => isNotEmpty(href)), distinctUntilChanged(), - map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)), - tap((request: RestRequest) => { - this.requestService.send(request); + mergeMap((endpointURL: string) => { + this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable); + const startTime: number = new Date().getTime(); + return this.requestService.getByHref(endpointURL).pipe( + map((requestEntry) => requestEntry?.request?.uuid), + hasValueOperator(), + distinctUntilChanged(), + switchMap((requestId) => this.rdbService.buildFromRequestUUID(requestId)), + // This skip ensures that if a stale object is present in the cache when you do a + // call it isn't immediately returned, but we wait until the remote data for the new request + // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a + // cached completed object + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), + tap((rd: RemoteData) => { + if (hasValue(rd) && rd.isStale) { + this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable); + } + }), + ); }), - mergeMap(() => this.fetchRequest(requestId)), - distinctUntilChanged()); + getFirstDataDefinition(), + ); + } + + /** + * Send a GET SubmissionRequest + * + * @param href + * Endpoint URL of the submission data + * @param useCachedVersionIfAvailable + * If this is true, the request will only be sent if there's no valid & cached version. Defaults to false + */ + private sendGetDataRequest(href: string, useCachedVersionIfAvailable = false) { + const requestId = this.requestService.generateRequestId(); + const request = new SubmissionRequest(requestId, href); + this.requestService.send(request, useCachedVersionIfAvailable); } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 957fab2b1e..c6853eb34b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -96,6 +96,7 @@ import { getRemoteDataPayload, } from '../../../../core/shared/operators'; import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; +import { SUBMISSION_LINKS_TO_FOLLOW } from '../../../../core/submission/resolver/submission-links-to-follow'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils'; import { SubmissionService } from '../../../../submission/submission.service'; @@ -450,7 +451,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo */ private setItem() { const submissionObject$ = this.submissionObjectService - .findById(this.model.submissionId, true, true, followLink('item'), followLink('collection')).pipe( + .findById(this.model.submissionId, true, true, ...SUBMISSION_LINKS_TO_FOLLOW).pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), ); diff --git a/src/app/submission/form/submission-form.component.spec.ts b/src/app/submission/form/submission-form.component.spec.ts index 4025b8c7d7..c399f9389b 100644 --- a/src/app/submission/form/submission-form.component.spec.ts +++ b/src/app/submission/form/submission-form.component.spec.ts @@ -226,7 +226,6 @@ describe('SubmissionFormComponent', () => { }); scheduler.flush(); - expect(comp.collectionId).toEqual(submissionObjectNew.collection.id); expect(comp.submissionDefinition).toEqual(submissionObjectNew.submissionDefinition); expect(comp.definitionId).toEqual(submissionObjectNew.submissionDefinition.name); expect(comp.sections).toEqual(submissionObjectNew.sections); @@ -264,7 +263,6 @@ describe('SubmissionFormComponent', () => { }); scheduler.flush(); - expect(comp.collectionId).toEqual('45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb'); expect(submissionServiceStub.resetSubmissionObject).not.toHaveBeenCalled(); done(); }); diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index ef0bcc11bc..10539839d9 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -288,13 +288,12 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * new submission object */ onCollectionChange(submissionObject: SubmissionObject) { - this.collectionId = (submissionObject.collection as Collection).id; if (this.definitionId !== (submissionObject.submissionDefinition as SubmissionDefinitionsModel).name) { this.sections = submissionObject.sections; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.definitionId = this.submissionDefinition.name; this.submissionService.resetSubmissionObject( - this.collectionId, + (submissionObject.collection as Collection).id, this.submissionId, submissionObject._links.self.href, this.submissionDefinition, diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts index d0f49d5700..09aa91124b 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -9,8 +9,8 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { SUBMISSION_LINKS_TO_FOLLOW } from '../core/submission/resolver/submission-links-to-follow'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -import { followLink } from '../shared/utils/follow-link-config.model'; export const workflowItemPageResolver: ResolveFn> = ( route: ActivatedRouteSnapshot, @@ -21,7 +21,7 @@ export const workflowItemPageResolver: ResolveFn> = ( route.params.id, true, false, - followLink('item'), + ...SUBMISSION_LINKS_TO_FOLLOW, ).pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts index 1d3b8e946d..e8d781b948 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts @@ -9,8 +9,8 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import { SUBMISSION_LINKS_TO_FOLLOW } from '../core/submission/resolver/submission-links-to-follow'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; -import { followLink } from '../shared/utils/follow-link-config.model'; /** * Method for resolving a workflow item based on the parameters in the current route @@ -28,7 +28,7 @@ export const workspaceItemPageResolver: ResolveFn> = ( return workspaceItemService.findById(route.params.id, true, false, - followLink('item'), + ...SUBMISSION_LINKS_TO_FOLLOW, ).pipe( getFirstCompletedRemoteData(), );