diff --git a/karma.conf.js b/karma.conf.js index a3b6803e6d..24cd067fd1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,7 +14,8 @@ module.exports = function (config) { require('karma-mocha-reporter'), ], client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser + captureConsole: false }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/dspace-angular'), diff --git a/server.ts b/server.ts index f857050ed5..ada6c9f040 100644 --- a/server.ts +++ b/server.ts @@ -171,7 +171,7 @@ function ngApp(req, res) { } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct CSR'); - res.sendFile(indexHtml); + res.sendFile(DIST_FOLDER + '/index.html'); } } diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts index e648a7f25e..11b146b294 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts @@ -3,7 +3,7 @@ import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -15,8 +15,8 @@ import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model' import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { - getAllSucceededRemoteDataPayload, - getFirstCompletedRemoteData + getFirstCompletedRemoteData, + getAllSucceededRemoteData } from '../../../core/shared/operators'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -39,7 +39,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { /** * A list of all the current EPeople within the repository or the result of the search */ - ePeople$: BehaviorSubject>> = new BehaviorSubject>>({} as any); + ePeople$: BehaviorSubject> = new BehaviorSubject(buildPaginatedList(new PageInfo(), [])); /** * A BehaviorSubject with the list of EpersonDtoModel objects made from the EPeople in the repository or * as the result of the search @@ -72,6 +72,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { currentSearchQuery: string; currentSearchScope: string; + /** + * The subscription for the search method + */ + searchSub: Subscription; + /** * List of subscriptions */ @@ -108,6 +113,29 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.isEPersonFormShown = true; } })); + this.subs.push(this.ePeople$.pipe( + switchMap((epeople: PaginatedList) => { + if (epeople.pageInfo.totalElements > 0) { + return combineLatest(...epeople.page.map((eperson) => { + return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe( + map((authorized) => { + const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); + epersonDtoModel.ableToDelete = authorized; + epersonDtoModel.eperson = eperson; + return epersonDtoModel; + }) + ); + })).pipe(map((dtos: EpersonDtoModel[]) => { + return buildPaginatedList(epeople.pageInfo, dtos); + })); + } else { + // if it's empty, simply forward the empty list + return [epeople]; + } + })).subscribe((value: PaginatedList) => { + this.ePeopleDto$.next(value); + this.pageInfoState$.next(value.pageInfo); + })); } /** @@ -138,34 +166,21 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.currentSearchScope = scope; this.config.currentPage = 1; } - this.subs.push(this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + if (hasValue(this.searchSub)) { + this.searchSub.unsubscribe(); + this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); + } + this.searchSub = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize - }).subscribe((peopleRD) => { - this.ePeople$.next(peopleRD); + }).pipe( + getAllSucceededRemoteData(), + ).subscribe((peopleRD) => { + this.ePeople$.next(peopleRD.payload); this.pageInfoState$.next(peopleRD.payload.pageInfo); } - )); - - this.subs.push(this.ePeople$.pipe( - getAllSucceededRemoteDataPayload(), - switchMap((epeople) => { - return combineLatest(...epeople.page.map((eperson) => { - return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe( - map((authorized) => { - const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); - epersonDtoModel.ableToDelete = authorized; - epersonDtoModel.eperson = eperson; - return epersonDtoModel; - }) - ); - })).pipe(map((dtos: EpersonDtoModel[]) => { - return buildPaginatedList(epeople.pageInfo, dtos); - })); - })).subscribe((value) => { - this.ePeopleDto$.next(value); - this.pageInfoState$.next(value.pageInfo); - })); + ); + this.subs.push(this.searchSub); } /** @@ -224,7 +239,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); } }); - }} + } + } }); } } @@ -261,16 +277,16 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { } /** - * This method will ensure that the page gets reset and that the cache is cleared + * This method will set everything to stale, which will cause the lists on this page to update. */ reset() { this.epersonService.getBrowseEndpoint().pipe( - switchMap((href) => this.requestService.removeByHrefSubstring(href)), - filter((isCached) => isCached), - take(1) - ).subscribe(() => { - this.cleanupSubscribes(); - this.initialisePage(); + take(1) + ).subscribe((href: string) => { + this.requestService.setStaleByHrefSubstring(href).pipe(take(1)).subscribe(() => { + this.epersonService.cancelEditEPerson(); + this.isEPersonFormShown = false; + }); }); } } diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index 59c019f6f6..3c284735a9 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -359,7 +359,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { }); const response = this.epersonService.updateEPerson(editedEperson); - response.pipe(take(1)).subscribe((rd: RemoteData) => { + response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name })); this.submitForm.emit(editedEperson); @@ -439,10 +439,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy { modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => { if (confirm) { if (hasValue(eperson.id)) { - this.epersonService.deleteEPerson(eperson).pipe(take(1)).subscribe((restResponse: RemoteData) => { + this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name })); - this.reset(); + this.submitForm.emit(); } else { this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); } diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index b17807b9d3..81e9513433 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -345,7 +345,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup === null) { this.groupDataService.cancelEditGroup(); - this.groupDataService.findByHref(groupSelfLink, false, followLink('subgroups'), followLink('epersons'), followLink('object')) + this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object')) .pipe( getFirstSucceededRemoteData(), getRemoteDataPayload()) diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html index d24bf35e2a..0ac67aff75 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -24,10 +24,10 @@ - @@ -42,7 +42,7 @@ - + {{ePerson.id}} {{ePerson.name}} @@ -70,7 +70,7 @@ - - @@ -36,7 +36,7 @@ - + {{group.id}} {{group.name}} @@ -65,17 +65,17 @@ -

{{messagePrefix + '.headSubgroups' | translate}}

- @@ -90,7 +90,7 @@ - + {{group.id}} {{group.name}} @@ -109,7 +109,7 @@ - diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index 2122a96cf3..9841d2b02e 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -1,12 +1,20 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + flush, + inject, + TestBed, + tick, + waitForAsync +} from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, BehaviorSubject } from 'rxjs'; import { RestResponse } from '../../../../../core/cache/response.models'; import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../../core/data/remote-data'; @@ -17,12 +25,16 @@ import { FormBuilderService } from '../../../../../shared/form/builder/form-buil import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; import { SubgroupsListComponent } from './subgroups-list.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject$, + createSuccessfulRemoteDataObject +} from '../../../../../shared/remote-data.utils'; import { RouterMock } from '../../../../../shared/mocks/router.mock'; import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock'; import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub'; +import { map } from 'rxjs/operators'; describe('SubgroupsListComponent', () => { let component: SubgroupsListComponent; @@ -43,7 +55,7 @@ describe('SubgroupsListComponent', () => { ePersonDataServiceStub = {}; groupsDataServiceStub = { activeGroup: activeGroup, - subgroups: subgroups, + subgroups$: new BehaviorSubject(subgroups), getActiveGroup(): Observable { return observableOf(this.activeGroup); }, @@ -51,7 +63,11 @@ describe('SubgroupsListComponent', () => { return this.activeGroup; }, findAllByHref(href: string): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.subgroups)); + return this.subgroups$.pipe( + map((currentGroups: Group[]) => { + return createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), currentGroups)); + }) + ); }, getGroupEditPageRouterLink(group: Group): string { return '/admin/access-control/groups/' + group.id; @@ -63,7 +79,7 @@ describe('SubgroupsListComponent', () => { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); }, addSubGroupToGroup(parentGroup, subgroup: Group): Observable { - this.subgroups = [...this.subgroups, subgroup]; + this.subgroups$.next([...this.subgroups$.getValue(), subgroup]); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -73,11 +89,11 @@ describe('SubgroupsListComponent', () => { // empty }, deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable { - this.subgroups = this.subgroups.find((group: Group) => { + this.subgroups$.next(this.subgroups$.getValue().filter((group: Group) => { if (group.id !== subgroup.id) { return group; } - }); + })); return observableOf(new RestResponse(true, 200, 'Success')); } }; diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index 662196fdba..fa1174792a 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; import { map, mergeMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../../core/data/remote-data'; @@ -13,9 +13,18 @@ import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../../../core/shared/operators'; -import { hasValue } from '../../../../../shared/empty.util'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; +import { NoContent } from '../../../../../core/shared/NoContent.model'; + +/** + * Keys to keep track of specific subscriptions + */ +enum SubKey { + Members, + ActiveGroup, + SearchResults, +} @Component({ selector: 'ds-subgroups-list', @@ -32,16 +41,16 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { /** * Result of search groups, initially all groups */ - groupsSearch: Observable>>; + searchResults$: BehaviorSubject>> = new BehaviorSubject(undefined); /** * List of all subgroups of group being edited */ - subgroupsOfGroup: Observable>>; + subGroups$: BehaviorSubject>> = new BehaviorSubject(undefined); /** - * List of subscriptions + * Map of active subscriptions */ - subs: Subscription[] = []; + subs: Map = new Map(); /** * Pagination config used to display the list of groups that are result of groups search @@ -84,10 +93,10 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.searchForm = this.formBuilder.group(({ query: '', })); - this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { if (activeGroup != null) { this.groupBeingEdited = activeGroup; - this.forceUpdateGroups(activeGroup); + this.retrieveSubGroups(this.config.currentPage); } })); } @@ -106,10 +115,26 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { * @param event */ onPageChange(event) { - this.subgroupsOfGroup = this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { - currentPage: event, - elementsPerPage: this.config.pageSize - }); + this.retrieveSubGroups(event); + } + + /** + * Retrieve the Subgroups that are members of the group + * + * @param page the number of the page to retrieve + * @private + */ + private retrieveSubGroups(page: number) { + this.unsubFrom(SubKey.Members); + this.subs.set( + SubKey.Members, + this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { + currentPage: page, + elementsPerPage: this.config.pageSize + } + ).subscribe((rd: RemoteData>) => { + this.subGroups$.next(rd); + })); } /** @@ -124,8 +149,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { return observableOf(false); } else { return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { - currentPage: 0, - elementsPerPage: Number.MAX_SAFE_INTEGER + currentPage: 1, + elementsPerPage: 9999 }) .pipe( getFirstSucceededRemoteData(), @@ -162,7 +187,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); - this.forceUpdateGroups(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -186,7 +210,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } }); - this.forceUpdateGroups(this.groupBeingEdited); } /** @@ -201,30 +224,37 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.configSearch.currentPage = 1; } this.searchDone = true; - this.groupsSearch = this.groupDataService.searchGroups(this.currentSearchQuery, { + + this.unsubFrom(SubKey.SearchResults); + this.subs.set(SubKey.SearchResults, this.groupDataService.searchGroups(this.currentSearchQuery, { currentPage: this.configSearch.currentPage, elementsPerPage: this.configSearch.pageSize - }); + }).subscribe((rd: RemoteData>) => { + this.searchResults$.next(rd); + })); } /** - * Force-update the list of groups by first clearing the cache of results of this active groups' subgroups, then performing a new REST call - * @param activeGroup Group currently being edited + * Unsubscribe from a subscription if it's still subscribed, and remove it from the map of + * active subscriptions + * + * @param key The key of the subscription to unsubscribe from + * @private */ - public forceUpdateGroups(activeGroup: Group) { - this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href); - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); - this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { - currentPage: this.config.currentPage, - elementsPerPage: this.config.pageSize - }); + private unsubFrom(key: SubKey) { + if (this.subs.has(key)) { + this.subs.get(key).unsubscribe(); + this.subs.delete(key); + } } /** * unsub all subscriptions */ ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + for (const key of this.subs.keys()) { + this.unsubFrom(key); + } } /** @@ -234,10 +264,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { * @param nameObject Object request was about * @param activeGroup Group currently being edited */ - showNotifications(messageSuffix: string, response: Observable>, nameObject: string, activeGroup: Group) { + showNotifications(messageSuffix: string, response: Observable>, nameObject: string, activeGroup: Group) { response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); } diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index 3202ee56c3..db5b1d3e3b 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -2,8 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription, Observable, ObservedValueOf, of as observableOf } from 'rxjs'; -import { filter } from 'rxjs/operators'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, + Observable, + of as observableOf +} from 'rxjs'; import { catchError, map, switchMap, take } from 'rxjs/operators'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -20,7 +25,8 @@ import { RouteService } from '../../../core/services/route.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload, - getFirstCompletedRemoteData + getFirstCompletedRemoteData, + getAllSucceededRemoteData } from '../../../core/shared/operators'; import { PageInfo } from '../../../core/shared/page-info.model'; import { hasValue } from '../../../shared/empty.util'; @@ -70,6 +76,11 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { // Current search in groups registry currentSearchQuery: string; + /** + * The subscription for the search method + */ + searchSub: Subscription; + /** * List of subscriptions */ @@ -93,6 +104,30 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { ngOnInit() { this.search({ query: this.currentSearchQuery }); + + this.subs.push(this.groups$.pipe( + getAllSucceededRemoteDataPayload(), + switchMap((groups: PaginatedList) => { + return observableCombineLatest(groups.page.map((group: Group) => { + return observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), + this.hasLinkedDSO(group) + ]).pipe( + map(([isAuthorized, hasLinkedDSO]: boolean[]) => { + const groupDtoModel: GroupDtoModel = new GroupDtoModel(); + groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; + groupDtoModel.group = group; + return groupDtoModel; + } + ) + ); + })).pipe(map((dtos: GroupDtoModel[]) => { + return buildPaginatedList(groups.pageInfo, dtos); + })); + })).subscribe((value: PaginatedList) => { + this.groupsDto$.next(value); + this.pageInfoState$.next(value.pageInfo); + })); } /** @@ -115,37 +150,20 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.currentSearchQuery = query; this.config.currentPage = 1; } - this.subs.push(this.groupService.searchGroups(this.currentSearchQuery.trim(), { + if (hasValue(this.searchSub)) { + this.searchSub.unsubscribe(); + this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); + } + this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize - }).pipe(getFirstCompletedRemoteData()) - .subscribe((groupsRD: RemoteData>) => { - this.groups$.next(groupsRD); - this.pageInfoState$.next(groupsRD.payload.pageInfo); - } - )); - - this.subs.push(this.groups$.pipe( - getAllSucceededRemoteDataPayload(), - switchMap((groups: PaginatedList) => { - return observableCombineLatest(...groups.page.map((group: Group) => { - return observableCombineLatest( - this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), - this.hasLinkedDSO(group), - (isAuthorized: ObservedValueOf>, hasLinkedDSO: ObservedValueOf>) => { - const groupDtoModel: GroupDtoModel = new GroupDtoModel(); - groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; - groupDtoModel.group = group; - return groupDtoModel; - } - ); - })).pipe(map((dtos: GroupDtoModel[]) => { - return buildPaginatedList(groups.pageInfo, dtos); - })); - })).subscribe((value: PaginatedList) => { - this.groupsDto$.next(value); - this.pageInfoState$.next(value.pageInfo); - })); + }).pipe( + getAllSucceededRemoteData() + ).subscribe((groupsRD: RemoteData>) => { + this.groups$.next(groupsRD); + this.pageInfoState$.next(groupsRD.payload.pageInfo); + }); + this.subs.push(this.searchSub); } /** @@ -168,16 +186,13 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { } /** - * This method will ensure that the page gets reset and that the cache is cleared + * This method will set everything to stale, which will cause the lists on this page to update. */ reset() { this.groupService.getBrowseEndpoint().pipe( - switchMap((href) => this.requestService.removeByHrefSubstring(href)), - filter((isCached) => isCached), take(1) - ).subscribe(() => { - this.cleanupSubscribes(); - this.search({ query: this.currentSearchQuery }); + ).subscribe((href: string) => { + this.requestService.setStaleByHrefSubstring(href); }); } diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index b6ffa97fdd..1c000c3c76 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -158,7 +158,6 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy { * Emit the updated/created field using the EventEmitter submitForm */ onSubmit() { - this.registryService.clearMetadataFieldRequests().subscribe(); this.registryService.getActiveMetadataField().pipe(take(1)).subscribe( (field) => { const values = { diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts index 3a12dcbe86..572bcc8a51 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -1,7 +1,13 @@ import { Component, OnInit } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, combineLatest as observableCombineLatest, combineLatest, Observable, zip } from 'rxjs'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + combineLatest, + Observable, + zip +} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @@ -11,9 +17,11 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload +} from '../../../core/shared/operators'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ @@ -89,8 +97,9 @@ export class MetadataSchemaComponent implements OnInit { this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe( switchMap(([schema, update]: [MetadataSchema, boolean]) => { if (update) { - return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), true, followLink('schema')); + this.needsUpdate$.next(false); } + return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), !update, true); }) ); } @@ -100,6 +109,7 @@ export class MetadataSchemaComponent implements OnInit { * a new REST call */ public forceUpdateFields() { + this.registryService.clearMetadataFieldRequests(); this.needsUpdate$.next(true); } @@ -159,7 +169,6 @@ export class MetadataSchemaComponent implements OnInit { * Delete all the selected metadata fields */ deleteFields() { - this.registryService.clearMetadataFieldRequests().subscribe(); this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( (fields) => { const tasks$ = []; @@ -173,6 +182,8 @@ export class MetadataSchemaComponent implements OnInit { const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); if (successResponses.length > 0) { this.showNotification(true, successResponses.length); + this.registryService.clearMetadataFieldRequests(); + } if (failedResponses.length > 0) { this.showNotification(false, failedResponses.length); diff --git a/src/app/+bitstream-page/bitstream-page.resolver.ts b/src/app/+bitstream-page/bitstream-page.resolver.ts index 4ac11202c2..a876b22d5e 100644 --- a/src/app/+bitstream-page/bitstream-page.resolver.ts +++ b/src/app/+bitstream-page/bitstream-page.resolver.ts @@ -23,7 +23,7 @@ export class BitstreamPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamService.findById(route.params.id, false, ...this.followLinks) + return this.bitstreamService.findById(route.params.id, true, false, ...this.followLinks) .pipe( getFirstCompletedRemoteData(), ); @@ -35,7 +35,7 @@ export class BitstreamPageResolver implements Resolve> { */ get followLinks(): FollowLinkConfig[] { return [ - followLink('bundle', undefined, true, followLink('item')), + followLink('bundle', undefined, true, true, true, followLink('item')), followLink('format') ]; } diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index af4153220f..8cdd99e70f 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -2,7 +2,7 @@

{{'collection.edit.item-mapper.head' | translate}}

-

+

{{'collection.edit.item-mapper.description' | translate}}

diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 7cd7871f6d..49b6a0d63c 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -6,7 +6,6 @@ import { CommonModule } from '@angular/common'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SearchFormComponent } from '../../shared/search-form/search-form.component'; import { ActivatedRoute, Router } from '@angular/router'; -import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { RouterStub } from '../../shared/testing/router.stub'; import { SearchServiceStub } from '../../shared/testing/search-service.stub'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -28,7 +27,7 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item import { ObjectSelectService } from '../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service.stub'; import { VarDirective } from '../../shared/utils/var.directive'; -import { of as observableOf, of } from 'rxjs'; +import { of as observableOf } from 'rxjs/internal/observable/of'; import { RouteService } from '../../core/services/route.service'; import { ErrorComponent } from '../../shared/error/error.component'; import { LoadingComponent } from '../../shared/loading/loading.component'; @@ -57,13 +56,16 @@ describe('CollectionItemMapperComponent', () => { id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', name: 'test-collection', _links: { + mappedItems: { + href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4/mappedItems' + }, self: { href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4' } } }); const mockCollectionRD: RemoteData = createSuccessfulRemoteDataObject(mockCollection); - const mockSearchOptions = of(new PaginatedSearchOptions({ + const mockSearchOptions = observableOf(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, @@ -82,25 +84,37 @@ describe('CollectionItemMapperComponent', () => { const searchConfigServiceStub = { paginatedSearchOptions: mockSearchOptions }; + const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { - mapToCollection: () => createSuccessfulRemoteDataObject$({}) + mapToCollection: () => createSuccessfulRemoteDataObject$({}), + findAllByHref: () => observableOf(emptyList) + }; + const activatedRouteStub = { + parent: { + data: observableOf({ + dso: mockCollectionRD + }) + }, + snapshot: { + queryParamMap: new Map([ + ['query', 'test'], + ]) + } }; - const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD }); const translateServiceStub = { - get: () => of('test-message of collection ' + mockCollection.name), + get: () => observableOf('test-message of collection ' + mockCollection.name), onLangChange: new EventEmitter(), onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() }; - const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const searchServiceStub = Object.assign(new SearchServiceStub(), { - search: () => of(emptyList), + search: () => observableOf(emptyList), /* tslint:disable:no-empty */ clearDiscoveryRequests: () => {} /* tslint:enable:no-empty */ }); const collectionDataServiceStub = { - getMappedItems: () => of(emptyList), + getMappedItems: () => observableOf(emptyList), /* tslint:disable:no-empty */ clearMappedItemsRequests: () => {} /* tslint:enable:no-empty */ diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index cd539fbad0..571b755897 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -1,6 +1,7 @@ import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; @@ -8,9 +9,10 @@ import { Collection } from '../../core/shared/collection.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { map, startWith, switchMap, take } from 'rxjs/operators'; import { - getRemoteDataPayload, - getFirstSucceededRemoteData, - toDSpaceObjectListRD + getRemoteDataPayload, + getFirstSucceededRemoteData, + toDSpaceObjectListRD, + getFirstCompletedRemoteData, getAllSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; @@ -52,12 +54,13 @@ export class CollectionItemMapperComponent implements OnInit { * A view on the tabset element * Used to switch tabs programmatically */ - @ViewChild('tabs') tabs; + @ViewChild('tabs', {static: false}) tabs; /** * The collection to map items to */ collectionRD$: Observable>; + collectionName$: Observable; /** * Search options @@ -101,11 +104,21 @@ export class CollectionItemMapperComponent implements OnInit { private notificationsService: NotificationsService, private itemDataService: ItemDataService, private collectionDataService: CollectionDataService, - private translateService: TranslateService) { + private translateService: TranslateService, + private dsoNameService: DSONameService) { } ngOnInit(): void { - this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable>; + this.collectionRD$ = this.route.parent.data.pipe( + map((data) => data.dso as RemoteData), + getFirstSucceededRemoteData() + ); + + this.collectionName$ = this.collectionRD$.pipe( + map((rd: RemoteData) => { + return this.dsoNameService.getName(rd.payload); + }) + ); this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadItemLists(); } @@ -123,26 +136,27 @@ export class CollectionItemMapperComponent implements OnInit { ); this.collectionItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options, shouldUpdate]) => { - if (shouldUpdate) { - return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { - sort: this.defaultSortOptions - }),followLink('owningCollection')); + if (shouldUpdate === true) { + this.shouldUpdate$.next(false); } + return this.itemDataService.findAllByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, { + sort: this.defaultSortOptions + }),!shouldUpdate, false, followLink('owningCollection')).pipe( + getAllSucceededRemoteData() + ); }) ); this.mappedItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options, shouldUpdate]) => { - if (shouldUpdate) { - return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { - query: this.buildQuery(collectionRD.payload.id, options.query), - scope: undefined, - dsoTypes: [DSpaceObjectType.ITEM], - sort: this.defaultSortOptions - }), 10000).pipe( - toDSpaceObjectListRD(), - startWith(undefined) - ); - } + return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { + query: this.buildQuery(collectionRD.payload.id, options.query), + scope: undefined, + dsoTypes: [DSpaceObjectType.ITEM], + sort: this.defaultSortOptions + }), 10000).pipe( + toDSpaceObjectListRD(), + startWith(undefined) + ); }) ); } @@ -157,8 +171,17 @@ export class CollectionItemMapperComponent implements OnInit { getFirstSucceededRemoteData(), map((collectionRD: RemoteData) => collectionRD.payload), switchMap((collection: Collection) => - observableCombineLatest(ids.map((id: string) => - remove ? this.itemDataService.removeMappingFromCollection(id, collection.id) : this.itemDataService.mapToCollection(id, collection._links.self.href) + observableCombineLatest(ids.map((id: string) => { + if (remove) { + return this.itemDataService.removeMappingFromCollection(id, collection.id).pipe( + getFirstCompletedRemoteData() + ); + } else { + return this.itemDataService.mapToCollection(id, collection._links.self.href).pipe( + getFirstCompletedRemoteData() + ); + } + } )) ) ); @@ -186,6 +209,7 @@ export class CollectionItemMapperComponent implements OnInit { successMessages.subscribe(([head, content]) => { this.notificationsService.success(head, content); }); + this.shouldUpdate$.next(true); } if (unsuccessful.length > 0) { const unsuccessMessages = observableCombineLatest( @@ -197,8 +221,6 @@ export class CollectionItemMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } - // Force an update on all lists and switch back to the first tab - this.shouldUpdate$.next(true); this.switchToFirstTab(); }); } diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 9d1df9b95d..7e44883a53 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -9,7 +9,6 @@ import { CreateCollectionPageGuard } from './create-collection-page/create-colle import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component'; import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver'; -import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; @@ -65,12 +64,6 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; path: '', component: CollectionPageComponent, pathMatch: 'full', - }, - { - path: '/edit/mapper', - component: CollectionItemMapperComponent, - pathMatch: 'full', - canActivate: [AuthenticatedGuard] } ], data: { diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index 8377afacfc..44d238fc97 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -23,7 +23,7 @@ export class CollectionPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.collectionService.findById(route.params.id, false, followLink('logo')).pipe( + return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe( getFirstCompletedRemoteData() ); } diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts index 818f064104..e41f0ebda4 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -1,5 +1,6 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; +import { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component'; import { EditCollectionPageComponent } from './edit-collection-page.component'; import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; @@ -86,7 +87,12 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit data: { title: 'collection.edit.tabs.authorizations.title', showBreadcrumbs: true } } ] - } + }, + { + path: 'mapper', + component: CollectionItemMapperComponent, + data: { title: 'collection.edit.tabs.item-mapper.title', showBreadcrumbs: true } + }, ] } ]) diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts index fe463fdfb5..719a04196f 100644 --- a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -23,7 +23,7 @@ export class ItemTemplatePageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemTemplateService.findByCollectionID(route.params.id, false, followLink('templateItemOf')).pipe( + return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( getFirstCompletedRemoteData(), ); } diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index 0f5538eec9..c5780f0a0f 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -25,6 +25,7 @@ export class CommunityPageResolver implements Resolve> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.communityService.findById( route.params.id, + true, false, followLink('logo'), followLink('subcommunities'), diff --git a/src/app/+import-external-page/import-external-page.module.ts b/src/app/+import-external-page/import-external-page.module.ts index bcf10b0752..1a0fd9e360 100644 --- a/src/app/+import-external-page/import-external-page.module.ts +++ b/src/app/+import-external-page/import-external-page.module.ts @@ -6,14 +6,18 @@ import { CoreModule } from '../core/core.module'; import { ImportExternalRoutingModule } from './import-external-routing.module'; import { SubmissionModule } from '../submission/submission.module'; import { ImportExternalPageComponent } from './import-external-page.component'; +import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; +import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; @NgModule({ imports: [ CommonModule, - SharedModule, + SharedModule.withEntryComponents(), CoreModule.forRoot(), ImportExternalRoutingModule, SubmissionModule, + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents() ], declarations: [ ImportExternalPageComponent diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 7c20de42e3..ad7ebba37f 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -1,16 +1,22 @@ -import { Component, OnInit } from '@angular/core'; -import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { + FieldUpdate, + FieldUpdates +} from '../../../core/data/object-updates/object-updates.reducer'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { Item } from '../../../core/shared/item.model'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, Data } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { first, map } from 'rxjs/operators'; +import { first, map, switchMap, tap } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { environment } from '../../../../environments/environment'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver'; +import { getAllSucceededRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-abstract-item-update', @@ -19,7 +25,7 @@ import { environment } from '../../../../environments/environment'; /** * Abstract component for managing object updates of an item */ -export class AbstractItemUpdateComponent extends AbstractTrackableComponent implements OnInit { +export class AbstractItemUpdateComponent extends AbstractTrackableComponent implements OnInit, OnDestroy { /** * The item to display the edit page for */ @@ -30,6 +36,12 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl */ updates$: Observable; + /** + * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request + * This is used to update the item in cache after bitstreams are deleted + */ + itemUpdateSubscription: Subscription; + constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, @@ -45,14 +57,20 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl * Initialize common properties between item-update components */ ngOnInit(): void { - observableCombineLatest(this.route.data, this.route.parent.data).pipe( - map(([data, parentData]) => Object.assign({}, data, parentData)), - map((data) => data.dso), - first(), - map((data: RemoteData) => data.payload) - ).subscribe((item: Item) => { - this.item = item; + this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( + map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)), + map((data: any) => data.dso), + tap((rd: RemoteData) => { + this.item = rd.payload; + }), + switchMap((rd: RemoteData) => { + return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW); + }), + getAllSucceededRemoteData() + ).subscribe((rd: RemoteData) => { + this.item = rd.payload; this.postItemInit(); + this.initializeUpdates(); }); this.discardTimeOut = environment.item.edit.undoTimeout; @@ -72,6 +90,12 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl this.initializeUpdates(); } + ngOnDestroy() { + if (hasValue(this.itemUpdateSubscription)) { + this.itemUpdateSubscription.unsubscribe(); + } + } + /** * Actions to perform after the item has been initialized * Abstract method: Should be overwritten in the sub class diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 3acbd77c40..20056a9ea4 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -90,6 +90,11 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; path: 'versionhistory', component: ItemVersionHistoryComponent, data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true } + }, + { + path: 'mapper', + component: ItemCollectionMapperComponent, + data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true } } ] }, diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 15c9edf644..fb193b24d4 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -79,7 +79,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataWithNotEmptyPayload(), map((item: Item) => this.linkService.resolveLink( item, - followLink('bundles', new FindListOptions(), true, followLink('bitstreams')) + followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')) )) ) as Observable; diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index e4e0932ce8..a2299be5ba 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -134,6 +134,7 @@ describe('ItemBitstreamsComponent', () => { }); itemService = Object.assign({ getBitstreams: () => createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])), + findByHref: () => createSuccessfulRemoteDataObject$(item), findById: () => createSuccessfulRemoteDataObject$(item), getBundles: () => createSuccessfulRemoteDataObject$(createPaginatedList([bundle])) }); diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index e5ed7750ef..d66c5d060d 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -12,11 +12,13 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; -import { Item } from '../../../core/shared/item.model'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; -import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { + FieldUpdate, + FieldUpdates +} from '../../../core/data/object-updates/object-updates.reducer'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { BundleDataService } from '../../../core/data/bundle-data.service'; @@ -93,14 +95,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } - /** - * Set up and initialize all fields - */ - ngOnInit(): void { - super.ngOnInit(); - this.initializeItemUpdate(); - } - /** * Actions to perform after the item has been initialized */ @@ -119,25 +113,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme this.notificationsPrefix = 'item.edit.bitstreams.notifications.'; } - /** - * Update the item (and view) when it's removed in the request cache - * Also re-initialize the original fields and updates - */ - initializeItemUpdate(): void { - this.itemUpdateSubscription = this.requestService.hasByHref$(this.item.self).pipe( - filter((exists: boolean) => !exists), - switchMap(() => this.itemService.findById(this.item.uuid)), - getFirstSucceededRemoteData(), - ).subscribe((itemRD: RemoteData) => { - if (hasValue(itemRD)) { - this.item = itemRD.payload; - this.postItemInit(); - this.initializeOriginalFields(); - this.initializeUpdates(); - this.cdRef.detectChanges(); - } - }); - } /** * Submit the current changes @@ -274,7 +249,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ reset() { this.refreshItemCache(); - this.initializeItemUpdate(); } /** diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 43bf7ecd02..a65111eaec 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -2,7 +2,7 @@

{{'item.edit.item-mapper.head' | translate}}

-

+

{{'item.edit.item-mapper.description' | translate}}

diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 0eba102414..21c1879828 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { of } from 'rxjs'; +import { of as observableOf } from 'rxjs/internal/observable/of'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -26,7 +26,6 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { SearchFormComponent } from '../../../shared/search-form/search-form.component'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service.stub'; @@ -59,7 +58,7 @@ describe('ItemCollectionMapperComponent', () => { name: 'test-item' }); const mockItemRD: RemoteData = createSuccessfulRemoteDataObject(mockItem); - const mockSearchOptions = of(new PaginatedSearchOptions({ + const mockSearchOptions = observableOf(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, @@ -81,20 +80,30 @@ describe('ItemCollectionMapperComponent', () => { const itemDataServiceStub = { mapToCollection: () => createSuccessfulRemoteDataObject$({}), removeMappingFromCollection: () => createSuccessfulRemoteDataObject$({}), - getMappedCollections: () => of(mockCollectionsRD), + getMappedCollectionsEndpoint: () => observableOf('rest/api/mappedCollectionsEndpoint'), + getMappedCollections: () => observableOf(mockCollectionsRD), /* tslint:disable:no-empty */ clearMappedCollectionsRequests: () => {} /* tslint:enable:no-empty */ }; + const collectionDataServiceStub = { + findAllByHref: () => observableOf(mockCollectionsRD) + }; const searchServiceStub = Object.assign(new SearchServiceStub(), { - search: () => of(mockCollectionsRD), + search: () => observableOf(mockCollectionsRD), /* tslint:disable:no-empty */ clearDiscoveryRequests: () => {} /* tslint:enable:no-empty */ }); - const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockItemRD }); + const activatedRouteStub = { + parent: { + data: observableOf({ + dso: mockItemRD + }) + } + }; const translateServiceStub = { - get: () => of('test-message of item ' + mockItem.name), + get: () => observableOf('test-message of item ' + mockItem.name), onLangChange: new EventEmitter(), onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() @@ -114,7 +123,7 @@ describe('ItemCollectionMapperComponent', () => { { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: TranslateService, useValue: translateServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: CollectionDataService, useValue: {} } + { provide: CollectionDataService, useValue: collectionDataServiceStub } ] }).compileComponents(); })); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 2b4f4bf564..e712ff2e35 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -1,6 +1,7 @@ import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { RemoteData } from '../../../core/data/remote-data'; @@ -11,15 +12,16 @@ import { getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getFirstSucceededRemoteData, - toDSpaceObjectListRD + toDSpaceObjectListRD, + getAllSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, startWith, switchMap, take } from 'rxjs/operators'; +import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { isNotEmpty } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SearchService } from '../../../core/shared/search/search.service'; @@ -50,6 +52,7 @@ export class ItemCollectionMapperComponent implements OnInit { * The item to map to collections */ itemRD$: Observable>; + itemName$: Observable; /** * Search options @@ -87,11 +90,22 @@ export class ItemCollectionMapperComponent implements OnInit { private notificationsService: NotificationsService, private itemDataService: ItemDataService, private collectionDataService: CollectionDataService, - private translateService: TranslateService) { + private translateService: TranslateService, + private dsoNameService: DSONameService) { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.parent.data.pipe( + take(1), + map((data) => data.dso), + ); + + this.itemName$ = this.itemRD$.pipe( + filter((rd: RemoteData) => hasValue(rd)), + map((rd: RemoteData) => { + return this.dsoNameService.getName(rd.payload); + }) + ); this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadCollectionLists(); } @@ -101,19 +115,28 @@ export class ItemCollectionMapperComponent implements OnInit { * Load mappedCollectionsRD$ to only obtain collections that don't own this item */ loadCollectionLists() { + console.log('loadCollectionLists'); this.shouldUpdate$ = new BehaviorSubject(true); - this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$, this.shouldUpdate$).pipe( - map(([itemRD, shouldUpdate]) => { - if (shouldUpdate) { - return itemRD.payload; + this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$.pipe(getFirstSucceededRemoteDataPayload()), this.shouldUpdate$).pipe( + switchMap(([item, shouldUpdate]) => { + if (shouldUpdate === true) { + this.shouldUpdate$.next(false); } + return this.collectionDataService.findAllByHref( + this.itemDataService.getMappedCollectionsEndpoint(item.id), + undefined, + !shouldUpdate, + false + ).pipe( + getAllSucceededRemoteData() + ); }), - switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); const owningCollectionRD$ = this.itemRD$.pipe( getFirstSucceededRemoteDataPayload(), - switchMap((item: Item) => this.collectionDataService.findOwningCollectionFor(item)) + switchMap((item: Item) => this.collectionDataService.findOwningCollectionFor(item)), + getAllSucceededRemoteData(), ); const itemCollectionsAndOptions$ = observableCombineLatest( this.itemCollectionsRD$, @@ -141,13 +164,11 @@ export class ItemCollectionMapperComponent implements OnInit { const itemIdAndExcludingIds$ = observableCombineLatest([ this.itemRD$.pipe( getFirstSucceededRemoteData(), - take(1), map((rd: RemoteData) => rd.payload), map((item: Item) => item.id) ), this.itemCollectionsRD$.pipe( getFirstSucceededRemoteData(), - take(1), map((rd: RemoteData>) => rd.payload.page), map((collections: Collection[]) => collections.map((collection: Collection) => collection.id)) ) @@ -155,7 +176,12 @@ export class ItemCollectionMapperComponent implements OnInit { // Map the item to the collections found in ids, excluding the collections the item is already mapped to const responses$ = itemIdAndExcludingIds$.pipe( - switchMap(([itemId, excludingIds]) => observableCombineLatest(this.filterIds(ids, excludingIds).map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) + switchMap(([itemId, excludingIds]) => + observableCombineLatest( + this.filterIds(ids, excludingIds).map((id: string) => + this.itemDataService.mapToCollection(itemId, id).pipe(getFirstCompletedRemoteData()) + )) + ) ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); @@ -169,7 +195,11 @@ export class ItemCollectionMapperComponent implements OnInit { const responses$ = this.itemRD$.pipe( getFirstSucceededRemoteData(), map((itemRD: RemoteData) => itemRD.payload.id), - switchMap((itemId: string) => observableCombineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id)))) + switchMap((itemId: string) => observableCombineLatest( + ids.map((id: string) => + this.itemDataService.removeMappingFromCollection(itemId, id).pipe(getFirstCompletedRemoteData()) + )) + ) ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); @@ -203,6 +233,7 @@ export class ItemCollectionMapperComponent implements OnInit { successMessages.subscribe(([head, content]) => { this.notificationsService.success(head, content); }); + this.shouldUpdate$.next(true); } if (unsuccessful.length > 0) { const unsuccessMessages = observableCombineLatest([ @@ -214,8 +245,6 @@ export class ItemCollectionMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } - // Force an update on all lists and switch back to the first tab - this.shouldUpdate$.next(true); this.switchToFirstTab(); }); } diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index ac58ccc42c..0a4df1bda5 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -228,7 +228,7 @@ describe('EditInPlaceFieldComponent', () => { })); it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { - expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, false, followLink('schema')); + expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema')); }); it('it should set metadataFieldSuggestions to the right value', () => { diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index a61fb21f50..2782747916 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -127,7 +127,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { */ findMetadataFieldSuggestions(query: string) { if (isNotEmpty(query)) { - return this.registryService.queryMetadataFields(query, null, false, followLink('schema')).pipe( + return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe( getFirstSucceededRemoteData(), metadataFieldsToString(), ).subscribe((fieldNames: string[]) => { diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index 8b810239b7..0f01efcc55 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -108,6 +108,11 @@ describe('ItemMetadataComponent', () => { [metadatum1.key]: [metadatum1], [metadatum2.key]: [metadatum2], [metadatum3.key]: [metadatum3] + }, + _links: { + self: { + href: 'https://rest.api/core/items/a36d8bd2-8e8c-4969-9b1f-a574c2064983' + } } }, { diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 960e40fd2f..2082143e05 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -11,7 +11,7 @@ import { } from '../../../../core/data/object-updates/object-updates.reducer'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { Item } from '../../../../core/shared/item.model'; -import { defaultIfEmpty, map, mergeMap, switchMap, take, } from 'rxjs/operators'; +import { defaultIfEmpty, map, mergeMap, switchMap, take, startWith } from 'rxjs/operators'; import { hasValue, hasValueOperator } from '../../../../shared/empty.util'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; @@ -311,12 +311,13 @@ export class EditRelationshipListComponent implements OnInit { return fieldUpdatesFiltered; }), )), + startWith({}), ); } private getItemRelationships() { this.linkService.resolveLink(this.item, - followLink('relationships', undefined, true, + followLink('relationships', undefined, true, true, true, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem'), diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index f3871ea98b..65fd49f795 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -133,6 +133,7 @@ describe('ItemRelationshipsComponent', () => { }; itemService = jasmine.createSpyObj('itemService', { + findByHref: createSuccessfulRemoteDataObject$(item), findById: createSuccessfulRemoteDataObject$(item) }); routeStub = { diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 3c5100e82e..785d548860 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -7,8 +7,12 @@ import { RelationshipIdentifiable, } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip } from 'rxjs'; +import { map, startWith, switchMap, take } from 'rxjs/operators'; +import { + combineLatest as observableCombineLatest, + of as observableOf, + zip as observableZip +} from 'rxjs'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -39,7 +43,6 @@ import { hasValue } from '../../../shared/empty.util'; */ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { - itemRD$: Observable>; /** * The allowed relationship types for this type of item as an observable list @@ -67,40 +70,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } - /** - * Set up and initialize all fields - */ - ngOnInit(): void { - super.ngOnInit(); - this.initializeItemUpdate(); - } - - /** - * Update the item (and view) when it's removed in the request cache - */ - public initializeItemUpdate(): void { - this.itemRD$ = this.requestService.hasByHref$(this.item.self).pipe( - filter((exists: boolean) => !exists), - switchMap(() => this.itemService.findById( - this.item.uuid, - true, - followLink('owningCollection'), - followLink('bundles'), - followLink('relationships')), - ), - filter((itemRD) => !!itemRD.statusCode), - ); - - this.itemRD$.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ).subscribe((item) => { - this.item = item; - this.cdr.detectChanges(); - this.initializeUpdates(); - }); - } - /** * Initialize the values and updates of the current item's relationship fields */ @@ -118,6 +87,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships( entityType.id, + true, + true, followLink('leftType'), followLink('rightType')) .pipe( @@ -183,11 +154,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { actions.forEach((action) => action.subscribe((response) => { if (response.length > 0) { - this.itemRD$.subscribe(() => { - this.initializeOriginalFields(); - this.cdr.detectChanges(); - this.displayNotifications(response); - }); + this.initializeOriginalFields(); + this.cdr.detectChanges(); + this.displayNotifications(response); } }) ); @@ -258,6 +227,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { + console.log('init'); return this.relationshipService.getRelatedItems(this.item).pipe( take(1), ).subscribe((items: Item[]) => { diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index d6c2344a30..ca3d5e65c7 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -67,6 +67,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On 'ORIGINAL', {elementsPerPage: this.pageSize, currentPage: pageNumber}, true, + true, followLink('format') )), tap((rd: RemoteData>) => { @@ -83,6 +84,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On 'LICENSE', {elementsPerPage: this.pageSize, currentPage: pageNumber}, true, + true, followLink('format') )), tap((rd: RemoteData>) => { diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 1b5eb05128..d90806bfc3 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -4,10 +4,17 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { ItemDataService } from '../core/data/item-data.service'; import { Item } from '../core/shared/item.model'; -import { followLink } from '../shared/utils/follow-link-config.model'; +import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { FindListOptions } from '../core/data/request.models'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('owningCollection'), + followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')), + followLink('relationships'), + followLink('version', undefined, true, true, true, followLink('versionhistory')), +]; + /** * This class represents a resolver that requests a specific item before the route is activated */ @@ -25,11 +32,9 @@ export class ItemPageResolver implements Resolve> { */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.itemService.findById(route.params.id, + true, false, - followLink('owningCollection'), - followLink('bundles', new FindListOptions(), true, followLink('bitstreams')), - followLink('relationships'), - followLink('version', undefined, true, followLink('versionhistory')), + ...ITEM_PAGE_LINKS_TO_FOLLOW ).pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index 3b79b6f1d8..3db7abfe84 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -1,6 +1,11 @@ import { Component, Input } from '@angular/core'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + zip as observableZip +} from 'rxjs'; import { RelationshipService } from '../../../core/data/relationship.service'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; @@ -82,7 +87,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList .map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) .map((metadatum: MetadataValue) => { if (metadatum.isVirtual) { - return this.relationshipService.findById(metadatum.virtualValue, false, followLink('leftItem'), followLink('rightItem')).pipe( + return this.relationshipService.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe( getFirstSucceededRemoteData(), switchMap((relRD: RemoteData) => observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index 6e57b8ae25..0df10d2b31 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -1,10 +1,11 @@ import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { Injectable } from '@angular/core'; -import { FindByIDRequest, IdentifierType } from '../core/data/request.models'; +import { IdentifierType } from '../core/data/request.models'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; +import { DSpaceObject } from '../core/shared/dspace-object.model'; interface LookupParams { type: IdentifierType; @@ -20,7 +21,7 @@ export class LookupGuard implements CanActivate { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = this.getLookupParams(route); return this.dsoService.findByIdAndIDType(params.id, params.type).pipe( - map((response: RemoteData) => response.hasFailed) + map((response: RemoteData) => response.hasFailed) ); } 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 6fc960f13c..4bb3eac513 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts @@ -24,6 +24,7 @@ export class WorkflowItemPageResolver implements Resolve> { return this.workflowItemService.findById(route.params.id, + true, false, followLink('item'), ).pipe( diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5c0392de15..be1233fd98 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,5 @@ import { APP_BASE_HREF, CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -40,6 +40,9 @@ import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component'; import { environment } from '../environments/environment'; import { BrowserModule } from '@angular/platform-browser'; import { ForbiddenComponent } from './forbidden/forbidden.component'; +import { AuthInterceptor } from './core/auth/auth.interceptor'; +import { LocaleInterceptor } from './core/locale/locale.interceptor'; +import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; export function getBase() { return environment.ui.nameSpace; @@ -94,6 +97,24 @@ const PROVIDERS = [ deps: [ Store ], multi: true }, + // register AuthInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + }, + // register LocaleInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: LocaleInterceptor, + multi: true + }, + // register XsrfInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: XsrfInterceptor, + multi: true + }, ...DYNAMIC_MATCHER_PROVIDERS, ]; diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 18111340f4..4315ddfea8 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -45,11 +45,7 @@ export class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), - map ((request: PostRequest) => { - request.responseMsToLive = 10 * 1000; - return request; - }), - tap((request: PostRequest) => this.requestService.configure(request)), + tap((request: PostRequest) => this.requestService.send(request)), mergeMap((request: PostRequest) => this.fetchRequest(request)), distinctUntilChanged()); } @@ -60,11 +56,7 @@ export class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), - map ((request: GetRequest) => { - request.forceBypassCache = true; - return request; - }), - tap((request: GetRequest) => this.requestService.configure(request)), + tap((request: GetRequest) => this.requestService.send(request)), mergeMap((request: GetRequest) => this.fetchRequest(request)), distinctUntilChanged()); } @@ -78,7 +70,7 @@ export class AuthRequestService { distinctUntilChanged(), map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: PostRequest) => this.requestService.configure(request)), + tap((request: PostRequest) => this.requestService.send(request)), switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), getFirstCompletedRemoteData(), map((response: RemoteData) => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index b6529aa9ca..fa29f1bc36 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -269,7 +269,7 @@ export class AuthService { let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); const options: HttpOptions = Object.create({ headers, responseType: 'text' }); - return this.authRequestService.getRequest('logout', options).pipe( + return this.authRequestService.postToEndpoint('logout', options).pipe( map((rd: RemoteData) => { const status = rd.payload; if (hasValue(status) && !status.authenticated) { diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 46c9d2fcc5..8e0638cddb 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -23,7 +23,7 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver[] { return [ - followLink('parentCommunity', undefined, true, + followLink('parentCommunity', undefined, true, true, true, followLink('parentCommunity') ) ]; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 9e3dd2f8ff..650bbd3301 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -29,7 +29,7 @@ export abstract class DSOBreadcrumbResolver> { const uuid = route.params.id; - return this.dataService.findById(uuid, false, ...this.followLinks).pipe( + return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), map((object: T) => { diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index ceabf5b4f3..e4d7e81e98 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -23,8 +23,8 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { */ get followLinks(): FollowLinkConfig[] { return [ - followLink('owningCollection', undefined, true, - followLink('parentCommunity', undefined, true, + followLink('owningCollection', undefined, true, true, true, + followLink('parentCommunity', undefined, true, true, true, followLink('parentCommunity')) ), followLink('bundles'), diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index 418bbb2f0e..1127748ca9 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -25,22 +25,22 @@ describe(`BrowseDefinitionDataService`, () => { describe(`findAll`, () => { it(`should call findAll on DataServiceImpl`, () => { - service.findAll(options, false, ...linksToFollow); - expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, false, ...linksToFollow); + service.findAll(options, true, false, ...linksToFollow); + expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow); }); }); describe(`findByHref`, () => { it(`should call findByHref on DataServiceImpl`, () => { - service.findByHref(hrefSingle, false, ...linksToFollow); - expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, false, ...linksToFollow); + service.findByHref(hrefSingle, true, false, ...linksToFollow); + expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, true, false, ...linksToFollow); }); }); describe(`findAllByHref`, () => { it(`should call findAllByHref on DataServiceImpl`, () => { - service.findAllByHref(hrefAll, options, false, ...linksToFollow); - expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, false, ...linksToFollow); + service.findAllByHref(hrefAll, options, true, false, ...linksToFollow); + expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, true, false, ...linksToFollow); }); }); diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index bc7e2f0479..31338417ca 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -63,42 +63,48 @@ export class BrowseDefinitionDataService { * info should be added to the objects * * @param options Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved * @return {Observable>>} * Return an observable that emits object list */ - findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.dataService.findAll(options, reRequestOnStale, ...linksToFollow); + findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.dataService.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Returns an observable of {@link RemoteData} of an {@link BrowseDefinition}, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link BrowseDefinition} - * @param href The url of {@link BrowseDefinition} we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param href The url of {@link BrowseDefinition} we want to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); + findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Returns a list of observables of {@link RemoteData} of {@link BrowseDefinition}s, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link BrowseDefinition} - * @param href The url of the {@link BrowseDefinition} we want to retrieve - * @param findListOptions Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param href The url of object we want to retrieve + * @param findListOptions Find list options object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); + findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 7802033535..89875b3069 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -11,8 +11,8 @@ import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseService } from './browse.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { GetRequest } from '../data/request.models'; +import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test'; +import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; describe('BrowseService', () => { let scheduler: TestScheduler; @@ -82,6 +82,7 @@ describe('BrowseService', () => { ]; let browseDefinitionDataService; + let hrefOnlyDataService; const getRequestEntry$ = (successful: boolean) => { return observableOf({ @@ -93,10 +94,12 @@ describe('BrowseService', () => { browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)) }); + hrefOnlyDataService = getMockHrefOnlyDataService(); return new BrowseService( requestService, halService, browseDefinitionDataService, + hrefOnlyDataService, rdbService ); } @@ -123,54 +126,40 @@ describe('BrowseService', () => { }); }); - describe('getBrowseEntriesFor and getBrowseItemsFor', () => { + describe('getBrowseEntriesFor and findList', () => { const mockAuthorName = 'Donald Smith'; beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(service, 'getBrowseDefinitions').and - .returnValue(hot('--a-', { - a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) - })); spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { - it('should configure a new BrowseEntriesRequest', () => { - const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href); + it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { + const expected = browseDefinitions[1]._links.entries.href; scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)); - - expect(rdbService.buildList).toHaveBeenCalled(); - + expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', { + a: expected + })); }); }); - describe('when getBrowseItemsFor is called with a valid browse definition id', () => { - it('should configure a new BrowseItemsRequest', () => { - const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName); + describe('when findList is called with a valid browse definition id', () => { + it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { + const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName; scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)); - - expect(rdbService.buildList).toHaveBeenCalled(); - + expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', { + a: expected + })); }); }); @@ -256,29 +245,19 @@ describe('BrowseService', () => { requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(service, 'getBrowseDefinitions').and - .returnValue(hot('--a-', { - a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) - })); spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { const expectedURL = browseDefinitions[1]._links.items.href + '?page=0&size=1'; - it('should configure a new BrowseItemsRequest', () => { - const expected = new GetRequest(requestService.generateRequestId(), expectedURL); - + it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getFirstItemFor(browseDefinitions[1].id); - - expect(rdbService.buildList).toHaveBeenCalled(); + expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', { + a: expectedURL + })); }); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 04dc5f234c..7e55d381a6 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core'; -import { Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, map, startWith, take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; -import { GetRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; @@ -21,6 +20,7 @@ import { import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; +import { HrefOnlyDataService } from '../data/href-only-data.service'; /** * The service handling all browse requests @@ -46,6 +46,7 @@ export class BrowseService { protected requestService: RequestService, protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, + private hrefOnlyDataService: HrefOnlyDataService, private rdb: RemoteDataBuildService, ) { } @@ -65,7 +66,7 @@ export class BrowseService { * @param options */ getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable>> { - return this.getBrowseDefinitions().pipe( + const href$ = this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(options.metadataDefinition), hasValueOperator(), map((_links: any) => { @@ -93,9 +94,9 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }), - getBrowseEntriesFor(this.requestService, this.rdb) + }) ); + return this.hrefOnlyDataService.findAllByHref(href$); } /** @@ -105,7 +106,7 @@ export class BrowseService { * @returns {Observable>>} */ getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable>> { - return this.getBrowseDefinitions().pipe( + const href$ = this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(options.metadataDefinition), hasValueOperator(), map((_links: any) => { @@ -136,8 +137,8 @@ export class BrowseService { } return href; }), - getBrowseItemsFor(this.requestService, this.rdb) ); + return this.hrefOnlyDataService.findAllByHref(href$); } /** @@ -146,7 +147,7 @@ export class BrowseService { * @param scope */ getFirstItemFor(definition: string, scope?: string): Observable> { - return this.getBrowseDefinitions().pipe( + const href$ = this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(definition), hasValueOperator(), map((_links: any) => { @@ -165,11 +166,14 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }), - getBrowseItemsFor(this.requestService, this.rdb), + }) + ); + + return this.hrefOnlyDataService.findAllByHref(href$).pipe( getFirstSucceededRemoteData(), getFirstOccurrence() ); + } /** @@ -177,9 +181,7 @@ export class BrowseService { * @param items */ getPrevBrowseItems(items: RemoteData>): Observable>> { - return observableOf(items.payload.prev).pipe( - getBrowseItemsFor(this.requestService, this.rdb) - ); + return this.hrefOnlyDataService.findAllByHref(items.payload.prev); } /** @@ -187,9 +189,7 @@ export class BrowseService { * @param items */ getNextBrowseItems(items: RemoteData>): Observable>> { - return observableOf(items.payload.next).pipe( - getBrowseItemsFor(this.requestService, this.rdb) - ); + return this.hrefOnlyDataService.findAllByHref(items.payload.next); } /** @@ -197,9 +197,7 @@ export class BrowseService { * @param entries */ getPrevBrowseEntries(entries: RemoteData>): Observable>> { - return observableOf(entries.payload.prev).pipe( - getBrowseEntriesFor(this.requestService, this.rdb) - ); + return this.hrefOnlyDataService.findAllByHref(entries.payload.prev); } /** @@ -207,9 +205,7 @@ export class BrowseService { * @param entries */ getNextBrowseEntries(entries: RemoteData>): Observable>> { - return observableOf(entries.payload.next).pipe( - getBrowseEntriesFor(this.requestService, this.rdb) - ); + return this.hrefOnlyDataService.findAllByHref(entries.payload.next); } /** @@ -241,39 +237,3 @@ export class BrowseService { } } - -/** - * Operator for turning a href into a PaginatedList of BrowseEntries - * @param requestService - * @param responseCache - * @param rdb - */ -export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => { - const requestId = requestService.generateRequestId(); - - source.pipe(take(1)).subscribe((href: string) => { - const request = new GetRequest(requestId, href); - requestService.configure(request); - }); - - return rdb.buildList(source); - }; - -/** - * Operator for turning a href into a PaginatedList of Items - * @param requestService - * @param responseCache - * @param rdb - */ -export const getBrowseItemsFor = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => { - const requestId = requestService.generateRequestId(); - - source.pipe(take(1)).subscribe((href: string) => { - const request = new GetRequest(requestId, href); - requestService.configure(request); - }); - - return rdb.buildList(source); - }; diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 544dd20bd4..1c41cfee86 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -38,11 +38,11 @@ class TestModel implements HALResource { @Injectable() class TestDataService { - findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { + findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { return 'findAllByHref'; } - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { + findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { return 'findByHref'; } } @@ -90,10 +90,10 @@ xdescribe('LinkService', () => { propertyName: 'predecessor' }); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); - service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); + service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }); it('should call dataservice.findByHref with the correct href and nested links', () => { - expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, followLink('successor')); + expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor')); }); }); describe(`when the linkdefinition concerns a list`, () => { @@ -105,10 +105,10 @@ xdescribe('LinkService', () => { isList: true }); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); - service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, followLink('successor'))); + service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor'))); }); it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => { - expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, followLink('successor')); + expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor')); }); }); describe('either way', () => { @@ -119,7 +119,7 @@ xdescribe('LinkService', () => { propertyName: 'predecessor' }); spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); - result = service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); + result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }); it('should call getLinkDefinition with the correct model and link', () => { @@ -144,7 +144,7 @@ xdescribe('LinkService', () => { }); it('should throw an error', () => { expect(() => { - service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); + service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }).toThrow(); }); }); @@ -160,7 +160,7 @@ xdescribe('LinkService', () => { }); it('should throw an error', () => { expect(() => { - service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))); + service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }).toThrow(); }); }); diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index fee5880d51..d340eec9d5 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -61,9 +61,9 @@ export class LinkService { try { if (matchingLinkDef.isList) { - model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, true, ...linkToFollow.linksToFollow); + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); } else { - model[linkToFollow.name] = service.findByHref(href, true, ...linkToFollow.linksToFollow); + model[linkToFollow.name] = service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); } } catch (e) { console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`); diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index fbaa52f956..cb193724a7 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => { let paginatedLinksToFollow; beforeEach(() => { paginatedLinksToFollow = [ - followLink('page', undefined, true, ...linksToFollow), + followLink('page', undefined, true, true, true, ...linksToFollow), ...linksToFollow ]; }); diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 0c1349e282..11815c133b 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -271,7 +271,7 @@ export class RemoteDataBuildService { * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ buildList(href$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.buildFromHref>(href$, followLink('page', undefined, false, ...linksToFollow)); + return this.buildFromHref>(href$, followLink('page', undefined, false, true, true, ...linksToFollow)); } /** diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 552da7cb08..363d566e00 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -94,14 +94,16 @@ export class ServerSyncBufferEffects { * @returns {Observable} ApplyPatchObjectCacheAction to be dispatched */ private applyPatch(href: string): Observable { - const patchObject = this.objectCache.getByHref(href).pipe(take(1)); + const patchObject = this.objectCache.getByHref(href).pipe( + take(1) + ); return patchObject.pipe( map((entry: ObjectCacheEntry) => { if (isNotEmpty(entry.patches)) { const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); if (isNotEmpty(flatPatch)) { - this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch)); + this.requestService.send(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch)); } } return new ApplyPatchObjectCacheAction(href); diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 48027be1fc..1eca35d223 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -58,12 +58,12 @@ describe('ConfigService', () => { describe('findByHref', () => { - it('should configure a new GetRequest', () => { + it('should send a new GetRequest', () => { const expected = new GetRequest(requestService.generateRequestId(), scopedEndpoint); scheduler.schedule(() => service.findByHref(scopedEndpoint).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); + expect(requestService.send).toHaveBeenCalledWith(expected, true); }); }); }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 60f60714eb..ddf909b5b0 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -52,8 +52,8 @@ export abstract class ConfigService { this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, this.linkPath); } - public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow).pipe( + public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( getFirstCompletedRemoteData(), map((rd: RemoteData) => { if (rd.hasFailed) { diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index 4c50f61a5b..a5c3f98060 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -34,7 +34,7 @@ export class SubmissionFormsConfigService extends ConfigService { super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionforms'); } - public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig[]) as Observable>; + public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig[]) as Observable>; } } diff --git a/src/app/core/config/submission-uploads-config.service.ts b/src/app/core/config/submission-uploads-config.service.ts index b70b08d7e1..a9e35a3183 100644 --- a/src/app/core/config/submission-uploads-config.service.ts +++ b/src/app/core/config/submission-uploads-config.service.ts @@ -36,7 +36,7 @@ export class SubmissionUploadsConfigService extends ConfigService { super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionuploads'); } - findByHref(href: string, reRequestOnStale = true, ...linksToFollow): Observable> { - return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig[]) as Observable>; + findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable> { + return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig[]) as Observable>; } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5d1456c09b..f73bfd0bdf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,8 +1,12 @@ import { CommonModule } from '@angular/common'; -import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; -import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { EffectsModule } from '@ngrx/effects'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; @@ -28,7 +32,6 @@ import { SidebarService } from '../shared/sidebar/sidebar.service'; import { UploaderService } from '../shared/uploader/uploader.service'; import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service'; import { AuthRequestService } from './auth/auth-request.service'; -import { AuthInterceptor } from './auth/auth.interceptor'; import { AuthenticatedGuard } from './auth/authenticated.guard'; import { AuthStatus } from './auth/models/auth-status.model'; import { BrowseService } from './browse/browse.service'; @@ -51,14 +54,12 @@ import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; import { DSOResponseParsingService } from './data/dso-response-parsing.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { ItemTypeDataService } from './data/entity-type-data.service'; import { EntityTypeService } from './data/entity-type.service'; import { ExternalSourceService } from './data/external-source.service'; import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; import { ItemDataService } from './data/item-data.service'; -import { LicenseDataService } from './data/license-data.service'; import { LookupRelationService } from './data/lookup-relation.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; @@ -130,7 +131,6 @@ import { ProcessDataService } from './data/processes/process-data.service'; import { ScriptDataService } from './data/processes/script-data.service'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; -import { LocaleInterceptor } from './locale/locale.interceptor'; import { ItemTemplateDataService } from './data/item-template-data.service'; import { TemplateItem } from './shared/template-item.model'; import { Feature } from './shared/feature.model'; @@ -160,6 +160,7 @@ import { EndUserAgreementService } from './end-user-agreement/end-user-agreement import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { UsageReport } from './statistics/models/usage-report.model'; +import { RootDataService } from './data/root-data.service'; import { Root } from './data/root.model'; /** @@ -263,8 +264,6 @@ const PROVIDERS = [ LookupRelationService, VersionDataService, VersionHistoryDataService, - LicenseDataService, - ItemTypeDataService, WorkflowActionDataService, ProcessDataService, ScriptDataService, @@ -279,18 +278,7 @@ const PROVIDERS = [ EndUserAgreementCurrentUserGuard, EndUserAgreementCookieGuard, EndUserAgreementService, - // register AuthInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: AuthInterceptor, - multi: true - }, - // register LocaleInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: LocaleInterceptor, - multi: true - }, + RootDataService, NotificationsService, FilteredDiscoveryPageResponseParsingService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, @@ -353,6 +341,7 @@ export const models = ShortLivedToken, Registration, UsageReport, + Root, ]; @NgModule({ diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 92b9fc96d2..df170397f8 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -55,8 +55,8 @@ describe('BitstreamDataService', () => { service.updateFormat(bitstream, format); }); - it('should configure a put request', () => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + it('should send a put request', () => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest)); }); }); }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 532281c309..1a16abc47f 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -25,7 +25,7 @@ import { RequestService } from './request.service'; import { BitstreamFormatDataService } from './bitstream-format-data.service'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { configureRequest } from '../shared/operators'; +import { sendRequest } from '../shared/operators'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PageInfo } from '../shared/page-info.model'; import { RequestEntryState } from './request.reducer'; @@ -62,15 +62,17 @@ export class BitstreamDataService extends DataService { /** * Retrieves the {@link Bitstream}s in a given bundle * - * @param bundle the bundle to retrieve bitstreams from - * @param options options for the find all request - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param bundle the bundle to retrieve bitstreams from + * @param options options for the find all request + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findAllByBundle(bundle: Bundle, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllByHref(bundle._links.bitstreams.href, options, reRequestOnStale, ...linksToFollow); + findAllByBundle(bundle: Bundle, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -120,7 +122,7 @@ export class BitstreamDataService extends DataService { return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( switchMap((bundleRD: RemoteData) => { if (isNotEmpty(bundleRD.payload)) { - return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 9999 }).pipe( map((bitstreamRD: RemoteData>) => { if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) => @@ -165,19 +167,22 @@ export class BitstreamDataService extends DataService { * The {@link Item} is technically redundant, but is available * in all current use cases, and having it simplifies this method * - * @param item the {@link Item} the {@link Bundle} is a part of - * @param bundleName the name of the {@link Bundle} we want to find {@link Bitstream}s for - * @param options the {@link FindListOptions} for the request - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param item the {@link Item} the {@link Bundle} is a part of + * @param bundleName the name of the {@link Bundle} we want to find + * {@link Bitstream}s for + * @param options the {@link FindListOptions} for the request + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.bundleService.findByItemAndName(item, bundleName).pipe( switchMap((bundleRD: RemoteData) => { if (bundleRD.hasSucceeded && hasValue(bundleRD.payload)) { - return this.findAllByBundle(bundleRD.payload, options, reRequestOnStale, ...linksToFollow); + return this.findAllByBundle(bundleRD.payload, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } else if (!bundleRD.hasSucceeded && bundleRD.statusCode === 404) { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []), new Date().getTime()); } else { @@ -209,7 +214,7 @@ export class BitstreamDataService extends DataService { options.headers = headers; return new PutRequest(requestId, bitstreamHref, formatHref, options); }), - configureRequest(this.requestService), + sendRequest(this.requestService), take(1) ).subscribe(() => { this.requestService.removeByHrefSubstring(bitstream.self + '/format'); diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index b754b49eb7..bb3fe2f064 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -73,7 +73,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -93,7 +93,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -115,7 +115,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -136,7 +136,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -160,7 +160,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -183,7 +183,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -206,7 +206,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -228,7 +228,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -250,7 +250,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), generateRequestId: 'request-id', @@ -270,7 +270,7 @@ describe('BitstreamFormatDataService', () => { beforeEach(waitForAsync(() => { scheduler = getTestScheduler(); requestService = jasmine.createSpyObj('requestService', { - configure: {}, + send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: hot('a', { a: responseCacheEntry }), generateRequestId: 'request-id', diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index e90d16ddef..424c9fbd99 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -19,7 +19,7 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; import { Bitstream } from '../shared/bitstream.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { configureRequest } from '../shared/operators'; +import { sendRequest } from '../shared/operators'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RemoteData } from './remote-data'; @@ -82,7 +82,7 @@ export class BitstreamFormatDataService extends DataService { distinctUntilChanged(), map((endpointURL: string) => new PutRequest(requestId, endpointURL, bitstreamFormat)), - configureRequest(this.requestService)).subscribe(); + sendRequest(this.requestService)).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); @@ -99,7 +99,7 @@ export class BitstreamFormatDataService extends DataService { map((endpointURL: string) => { return new PostRequest(requestId, endpointURL, bitstreamFormat); }), - configureRequest(this.requestService) + sendRequest(this.requestService) ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); diff --git a/src/app/core/data/bundle-data.service.spec.ts b/src/app/core/data/bundle-data.service.spec.ts index 440ed0457e..ed149a624f 100644 --- a/src/app/core/data/bundle-data.service.spec.ts +++ b/src/app/core/data/bundle-data.service.spec.ts @@ -81,7 +81,7 @@ describe('BundleDataService', () => { }); it('should call findAllByHref with the item\'s bundles link', () => { - expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true); + expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true, true); }); }); diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 13b35ecb23..bff21d2c8d 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -50,28 +50,34 @@ export class BundleDataService extends DataService { /** * Retrieve all {@link Bundle}s in the given {@link Item} * - * @param item the {@link Item} the {@link Bundle}s are a part of - * @param options the {@link FindListOptions} for the request - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow the {@link FollowLinkConfig}s for the request + * @param item the {@link Item} the {@link Bundle}s are a part of + * @param options the {@link FindListOptions} for the request + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findAllByItem(item: Item, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllByHref(item._links.bundles.href, options, reRequestOnStale, ...linksToFollow); + findAllByItem(item: Item, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllByHref(item._links.bundles.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Retrieve a {@link Bundle} in the given {@link Item} by name * - * @param item the {@link Item} the {@link Bundle}s are a part of - * @param bundleName the name of the {@link Bundle} to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow the {@link FollowLinkConfig}s for the request + * @param item the {@link Item} the {@link Bundle}s are a part of + * @param bundleName the name of the {@link Bundle} to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ // TODO should be implemented rest side - findByItemAndName(item: Item, bundleName: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, reRequestOnStale, ...linksToFollow).pipe( + findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( map((rd: RemoteData>) => { if (hasValue(rd.payload) && hasValue(rd.payload.page)) { const matchingBundle = rd.payload.page.find((bundle: Bundle) => @@ -129,7 +135,7 @@ export class BundleDataService extends DataService { take(1) ).subscribe((href) => { const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); + this.requestService.send(request, true); }); return this.rdbService.buildList(hrefObs, ...linksToFollow); diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 0045b21dc1..00aede27a6 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -6,7 +6,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { fakeAsync, tick } from '@angular/core/testing'; -import { ContentSourceRequest, GetRequest, UpdateContentSourceRequest } from './request.models'; +import { ContentSourceRequest, UpdateContentSourceRequest } from './request.models'; import { ContentSource } from '../shared/content-source.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -87,10 +87,10 @@ describe('CollectionDataService', () => { contentSource$ = service.getContentSource(collectionId); }); - it('should configure a new ContentSourceRequest', fakeAsync(() => { + it('should send a new ContentSourceRequest', fakeAsync(() => { contentSource$.subscribe(); tick(); - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest), true); })); }); @@ -103,25 +103,13 @@ describe('CollectionDataService', () => { returnedContentSource$ = service.updateContentSource(collectionId, contentSource); }); - it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { + it('should send a new UpdateContentSourceRequest', fakeAsync(() => { returnedContentSource$.subscribe(); tick(); - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); })); }); - describe('getMappedItems', () => { - let result; - - beforeEach(() => { - result = service.getMappedItems('collection-id'); - }); - - it('should configure a GET request', () => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); - }); - }); - describe('when calling getAuthorizedCollection', () => { beforeEach(() => { scheduler = getTestScheduler(); @@ -175,10 +163,10 @@ describe('CollectionDataService', () => { returnedContentSource$ = service.updateContentSource(collectionId, contentSource); }); - it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { + it('should send a new UpdateContentSourceRequest', fakeAsync(() => { returnedContentSource$.subscribe(); tick(); - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest)); })); it('should display an error notification', fakeAsync(() => { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index bd8fa91bab..f58f36450f 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -3,12 +3,11 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { INotification } from '../../shared/notifications/models/notification.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -20,26 +19,19 @@ import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { Collection } from '../shared/collection.model'; import { COLLECTION } from '../shared/collection.resource-type'; import { ContentSource } from '../shared/content-source.model'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { GenericConstructor } from '../shared/generic-constructor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { - configureRequest, - getFirstCompletedRemoteData -} from '../shared/operators'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; import { PaginatedList } from './paginated-list.model'; -import { ResponseParsingService } from './parsing.service'; import { RemoteData } from './remote-data'; import { ContentSourceRequest, FindListOptions, - GetRequest, - UpdateContentSourceRequest + UpdateContentSourceRequest, + RestRequest } from './request.models'; import { RequestService } from './request.service'; import { BitstreamDataService } from './bitstream-data.service'; @@ -70,20 +62,25 @@ export class CollectionDataService extends ComColDataService { /** * Get all collections the user is authorized to submit to * - * @param query limit the returned collection to those with metadata values matching the query terms. - * @param options The [[FindListOptions]] object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale + * @param query limit the returned collection to those with metadata values + * matching the query terms. + * @param options The [[FindListOptions]] object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved * @return Observable>> * collection list */ - getAuthorizedCollection(query: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const searchHref = 'findSubmitAuthorized'; options = Object.assign({}, options, { searchParams: [new RequestParam('query', query)] }); - return this.searchBy(searchHref, options, reRequestOnStale, ...linksToFollow).pipe( + return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } @@ -149,7 +146,7 @@ export class CollectionDataService extends ComColDataService { href$.subscribe((href: string) => { const request = new ContentSourceRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); + this.requestService.send(request, true); }); return this.rdbService.buildSingle(href$); @@ -175,9 +172,7 @@ export class CollectionDataService extends ComColDataService { ); // Execute the post/put request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); + request$.subscribe((request: RestRequest) => this.requestService.send(request)); // Return updated ContentSource return this.rdbService.buildFromRequestUUID(requestId).pipe( @@ -205,48 +200,6 @@ export class CollectionDataService extends ComColDataService { ); } - /** - * Fetches the endpoint used for mapping items to a collection - * @param collectionId The id of the collection to map items to - */ - getMappedItemsEndpoint(collectionId): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, collectionId)), - map((endpoint: string) => `${endpoint}/mappedItems`) - ); - } - - /** - * Fetches a list of items that are mapped to a collection - * @param collectionId The id of the collection - * @param searchOptions Search options to sort or filter out items - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const requestUuid = this.requestService.generateRequestId(); - - const href$ = this.getMappedItemsEndpoint(collectionId).pipe( - isNotEmptyOperator(), - distinctUntilChanged(), - map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) - ); - - href$.pipe( - map((endpoint: string) => { - const request = new GetRequest(requestUuid, endpoint); - return Object.assign(request, { - responseMsToLive: 0, - getResponseParser(): GenericConstructor { - return DSOResponseParsingService; - } - }); - }), - configureRequest(this.requestService) - ).subscribe(); - - return this.rdbService.buildList(href$, ...linksToFollow); - } - protected getFindByParentHref(parentUUID: string): Observable { return this.halService.getEndpoint('communities').pipe( switchMap((communityEndpointHref: string) => @@ -261,5 +214,4 @@ export class CollectionDataService extends ComColDataService { findOwningCollectionFor(item: Item): Observable> { return this.findByHref(item._links.owningCollection.href); } - } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 80e70bed51..94b050855a 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -13,14 +13,16 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { FindByIDRequest, FindListOptions } from './request.models'; +import { FindListOptions, GetRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { - createFailedRemoteDataObject$, createNoContentRemoteDataObject$, + createFailedRemoteDataObject$, + createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { BitstreamDataService } from './bitstream-data.service'; +import { take } from 'rxjs/operators'; const LINK_NAME = 'test'; @@ -147,23 +149,23 @@ describe('ComColDataService', () => { scheduler = getTestScheduler(); }); - it('should configure a new FindByIDRequest for the scope Community', () => { + it('should send a new FindByIDRequest for the scope Community', () => { cds = initMockCommunityDataService(); requestService = getMockRequestService(getRequestEntry$(true)); objectCache = initMockObjectCacheService(); service = initTestService(); - const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); + const expected = new GetRequest(requestService.generateRequestId(), communityEndpoint); scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); + expect(requestService.send).toHaveBeenCalledWith(expected, true); }); describe('if the scope Community can\'t be found', () => { it('should throw an error', () => { - const result = service.getBrowseEndpoint(options); + const result = service.getBrowseEndpoint(options).pipe(take(1)); const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`)); expect(result).toBeObservable(expected); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 3098bdf4be..2dcffeb961 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -7,7 +7,7 @@ import { HALLink } from '../shared/hal-link.model'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; -import { FindByIDRequest, FindListOptions } from './request.models'; +import { FindListOptions } from './request.models'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -42,11 +42,10 @@ export abstract class ComColDataService extend const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)), filter((href: string) => isNotEmpty(href)), - take(1), - tap((href: string) => { - const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); - this.requestService.configure(request); - })); + take(1) + ); + + this.createAndSendGetRequest(scopeCommunityHrefObs, true); return scopeCommunityHrefObs.pipe( switchMap((href: string) => this.rdbService.buildSingle(href)), @@ -71,7 +70,7 @@ export abstract class ComColDataService extend const href$ = this.getFindByParentHref(parentUUID).pipe( map((href: string) => this.buildHrefFromFindOptions(href, options)) ); - return this.findList(href$, options); + return this.findAllByHref(href$); } /** diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index daf8639cb4..6fbc2f9778 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -3,8 +3,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { filter, switchMap, take } from 'rxjs/operators'; -import { hasValue } from '../../shared/empty.util'; +import { switchMap } from 'rxjs/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -17,7 +16,7 @@ import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { FindListOptions, FindListRequest } from './request.models'; +import { FindListOptions } from './request.models'; import { RequestService } from './request.service'; import { BitstreamDataService } from './bitstream-data.service'; @@ -48,15 +47,7 @@ export class CommunityDataService extends ComColDataService { findTop(options: FindListOptions = {}): Observable>> { const hrefObs = this.getFindAllHref(options, this.topLinkPath); - hrefObs.pipe( - filter((href: string) => hasValue(href)), - take(1)) - .subscribe((href: string) => { - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - this.requestService.configure(request); - }); - - return this.rdbService.buildList(hrefObs) as Observable>>; + return this.findAllByHref(hrefObs, undefined); } protected getFindByParentHref(parentUUID: string): Observable { diff --git a/src/app/core/data/configuration-data.service.spec.ts b/src/app/core/data/configuration-data.service.spec.ts index 3d4fc32a7b..7077f098e0 100644 --- a/src/app/core/data/configuration-data.service.spec.ts +++ b/src/app/core/data/configuration-data.service.spec.ts @@ -2,7 +2,7 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindByIDRequest } from './request.models'; +import { GetRequest } from './request.models'; import { RequestService } from './request.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -34,7 +34,7 @@ describe('ConfigurationDataService', () => { }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - configure: true + send: true }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { @@ -67,11 +67,11 @@ describe('ConfigurationDataService', () => { expect(halService.getEndpoint).toHaveBeenCalledWith('properties'); }); - it('should configure the proper FindByIDRequest', () => { + it('should send the proper FindByIDRequest', () => { scheduler.schedule(() => service.findByPropertyName(testObject.name)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.name)); + expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL), true); }); it('should return a RemoteData for the object with the given name', () => { diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index cf82fdcc2e..82d2271c10 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -212,7 +212,7 @@ describe('DataService', () => { it('should include nested linksToFollow 3lvl', () => { const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`; - (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))).subscribe((value) => { + (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -247,7 +247,7 @@ describe('DataService', () => { it('should include nested linksToFollow 3lvl', () => { const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); + const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, true, true,followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))); expect(result).toEqual(expected); }); }); @@ -268,8 +268,8 @@ describe('DataService', () => { service.patch(dso, operations); }); - it('should configure a PatchRequest', () => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PatchRequest)); + it('should send a PatchRequest', () => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest)); }); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 73e623200a..e98de0f77d 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,16 +1,17 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch'; -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { distinctUntilChanged, filter, find, - first, map, mergeMap, take, - takeWhile, switchMap, tap, + takeWhile, + switchMap, + tap, } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; @@ -25,22 +26,18 @@ import { CoreState } from '../core.reducers'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { - getRemoteDataPayload, - getFirstSucceededRemoteData, -} from '../shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData, } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { ChangeAnalyzer } from './change-analyzer'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { CreateRequest, - FindByIDRequest, - FindListOptions, - FindListRequest, GetRequest, + FindListOptions, PatchRequest, - PutRequest, DeleteRequest + PutRequest, + DeleteRequest } from './request.models'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; @@ -159,23 +156,23 @@ export abstract class DataService implements UpdateDa if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ - args = [...args, `page=${options.currentPage - 1}`]; + args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`); } if (hasValue(options.elementsPerPage)) { - args = [...args, `size=${options.elementsPerPage}`]; + args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`); } if (hasValue(options.sort)) { - args = [...args, `sort=${options.sort.field},${options.sort.direction}`]; + args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`); } if (hasValue(options.startsWith)) { - args = [...args, `startsWith=${options.startsWith}`]; + args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`); } if (hasValue(options.searchParams)) { options.searchParams.forEach((param: RequestParam) => { - args = [...args, `${param.fieldName}=${param.fieldValue}`]; + args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`); }); } - args = this.addEmbedParams(args, ...linksToFollow); + args = this.addEmbedParams(href, args, ...linksToFollow); if (isNotEmpty(args)) { return new URLCombiner(href, `?${args.join('&')}`).toString(); } else { @@ -198,11 +195,11 @@ export abstract class DataService implements UpdateDa let args = []; if (hasValue(params)) { params.forEach((param: RequestParam) => { - args.push(`${param.fieldName}=${param.fieldValue}`); + args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`); }); } - args = this.addEmbedParams(args, ...linksToFollow); + args = this.addEmbedParams(href, args, ...linksToFollow); if (isNotEmpty(args)) { return new URLCombiner(href, `?${args.join('&')}`).toString(); @@ -212,20 +209,39 @@ export abstract class DataService implements UpdateDa } /** * Adds the embed options to the link for the request + * @param href The href the params are to be added to * @param args params for the query string * @param linksToFollow links we want to embed in query string if shouldEmbed is true */ - protected addEmbedParams(args: string[], ...linksToFollow: FollowLinkConfig[]) { + protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig[]) { linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { if (linkToFollow !== undefined && linkToFollow.shouldEmbed) { const embedString = 'embed=' + String(linkToFollow.name); const embedWithNestedString = this.addNestedEmbeds(embedString, ...linkToFollow.linksToFollow); - args = [...args, embedWithNestedString]; + args = this.addHrefArg(href, args, embedWithNestedString); } }); return args; } + /** + * Add a new argument to the list of arguments, only if it doesn't already exist in the given href, + * or the current list of arguments + * + * @param href The href the arguments are to be added to + * @param currentArgs The current list of arguments + * @param newArg The new argument to add + * @return The next list of arguments, with newArg included if it wasn't already. + * Note this function will not modify any of the input params. + */ + protected addHrefArg(href: string, currentArgs: string[], newArg: string): string[] { + if (href.includes(newArg) || currentArgs.includes(newArg)) { + return [...currentArgs]; + } else { + return [...currentArgs, newArg]; + } + } + /** * Add the nested followLinks to the embed param, recursively, separated by a / * @param embedString embedString so far (recursive) @@ -248,45 +264,18 @@ export abstract class DataService implements UpdateDa * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded * info should be added to the objects * - * @param options Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param options Find list options object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved * @return {Observable>>} * Return an observable that emits object list */ - findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findList(this.getFindAllHref(options), options, reRequestOnStale, ...linksToFollow); - } - - /** - * Returns an observable of {@link RemoteData} of an object, based on href observable, - * with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href$ Observable of href of object we want to retrieve - * @param options Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved - */ - protected findList(href$, options: FindListOptions, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { - const requestId = this.requestService.generateRequestId(); - - href$.pipe( - first((href: string) => hasValue(href))) - .subscribe((href: string) => { - const request = new FindListRequest(requestId, href, options); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }); - - return this.rdbService.buildList(href$, ...linksToFollow).pipe( - reRequestStaleRemoteData(reRequestOnStale, () => - this.findList(href$, options, reRequestOnStale, ...linksToFollow)) - ) as Observable>>; + findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -312,79 +301,108 @@ export abstract class DataService implements UpdateDa /** * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param id ID of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param id ID of object we want to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findById(id: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - const requestId = this.requestService.generateRequestId(); - - const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow).pipe( - isNotEmptyOperator(), - take(1) - ); - - href$.subscribe((href: string) => { - const request = new FindByIDRequest(requestId, href, id); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }); - - return this.rdbService.buildSingle(href$, ...linksToFollow).pipe( - reRequestStaleRemoteData(reRequestOnStale, () => - this.findById(id, reRequestOnStale, ...linksToFollow)) - ); + findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow); + return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href The url of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param href$ The url of object we want to retrieve. Can be a string or + * an Observable + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - const requestHref = this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow); - const requestId = this.requestService.generateRequestId(); - const request = new GetRequest(requestId, requestHref); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; + findByHref(href$: string | Observable, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + if (typeof href$ === 'string') { + href$ = observableOf(href$); } - this.requestService.configure(request); - return this.rdbService.buildSingle(href, ...linksToFollow).pipe( + + const requestHref$ = href$.pipe( + isNotEmptyOperator(), + take(1), + map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)) + ); + + this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); + + return this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( reRequestStaleRemoteData(reRequestOnStale, () => - this.findByHref(href, reRequestOnStale, ...linksToFollow)) + this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) ); } /** * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list * of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href The url of object we want to retrieve - * @param findListOptions Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s - * should be automatically resolved + * @param href$ The url of object we want to retrieve. Can be a string or + * an Observable + * @param findListOptions Find list options object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const requestHref = this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow); - const requestId = this.requestService.generateRequestId(); - const request = new GetRequest(requestId, requestHref); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; + findAllByHref(href$: string | Observable, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + if (typeof href$ === 'string') { + href$ = observableOf(href$); } - this.requestService.configure(request); - return this.rdbService.buildList(requestHref, ...linksToFollow).pipe( - reRequestStaleRemoteData(reRequestOnStale, () => - this.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow)) + + const requestHref$ = href$.pipe( + isNotEmptyOperator(), + take(1), + map((href: string) => this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow)) ); + + this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); + + return this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.findAllByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) + ); + } + + /** + * Create a GET request for the given href, and send it. + * + * @param href$ The url of object we want to retrieve. Can be a string or + * an Observable + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + */ + protected createAndSendGetRequest(href$: string | Observable, useCachedVersionIfAvailable = true): void { + if (isNotEmpty(href$)) { + if (typeof href$ === 'string') { + href$ = observableOf(href$); + } + + href$.pipe( + isNotEmptyOperator(), + take(1) + ).subscribe((href: string) => { + const requestId = this.requestService.generateRequestId(); + const request = new GetRequest(requestId, href); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.send(request, useCachedVersionIfAvailable); + }); + } } /** @@ -401,32 +419,21 @@ export abstract class DataService implements UpdateDa /** * Make a new FindListRequest with given search method * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow The array of [[FollowLinkConfig]] + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved * @return {Observable>} * Return an observable that emits response from the server */ - searchBy(searchMethod: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const requestId = this.requestService.generateRequestId(); + searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - hrefObs.pipe( - find((href: string) => hasValue(href)) - ).subscribe((href: string) => { - const request = new FindListRequest(requestId, href, options); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }); - - return this.rdbService.buildList(hrefObs, ...linksToFollow).pipe( - reRequestStaleRemoteData(reRequestOnStale, () => - this.searchBy(searchMethod, options, reRequestOnStale, ...linksToFollow)) - ); + return this.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -447,14 +454,14 @@ export abstract class DataService implements UpdateDa if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } - this.requestService.configure(request); + this.requestService.send(request); }); return this.rdbService.buildFromRequestUUID(requestId); } createPatchFromCache(object: T): Observable { - const oldVersion$ = this.findByHref(object._links.self.href, false); + const oldVersion$ = this.findByHref(object._links.self.href, true, false); return oldVersion$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), @@ -475,7 +482,7 @@ export abstract class DataService implements UpdateDa request.responseMsToLive = this.responseMsToLive; } - this.requestService.configure(request); + this.requestService.send(request); return this.rdbService.buildFromRequestUUID(requestId); } @@ -492,7 +499,7 @@ export abstract class DataService implements UpdateDa if (isNotEmpty(operations)) { this.objectCache.addPatch(object._links.self.href, operations); } - return this.findByHref(object._links.self.href, true); + return this.findByHref(object._links.self.href, true, true); } ) ); @@ -524,7 +531,7 @@ export abstract class DataService implements UpdateDa if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } - this.requestService.configure(request); + this.requestService.send(request); }); const result$ = this.rdbService.buildFromRequestUUID(requestId); @@ -579,7 +586,7 @@ export abstract class DataService implements UpdateDa if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } - this.requestService.configure(request); + this.requestService.send(request); return this.rdbService.buildFromRequestUUID(requestId); } diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts index a9aaf473c3..602a447d52 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -9,7 +9,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DsoRedirectDataService } from './dso-redirect-data.service'; -import { FindByIDRequest, IdentifierType } from './request.models'; +import { GetRequest, IdentifierType } from './request.models'; import { RequestService } from './request.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; @@ -42,7 +42,7 @@ describe('DsoRedirectDataService', () => { }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - configure: true + send: true }); router = { navigate: jasmine.createSpy('navigate') @@ -93,18 +93,18 @@ describe('DsoRedirectDataService', () => { expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); }); - it('should configure the proper FindByIDRequest for uuid', () => { + it('should send the proper FindByIDRequest for uuid', () => { scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID)); + expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestUUIDURL), true); }); - it('should configure the proper FindByIDRequest for handle', () => { + it('should send the proper FindByIDRequest for handle', () => { scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle)); + expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestHandleURL), true); }); it('should navigate to item route', () => { @@ -162,7 +162,7 @@ describe('DsoRedirectDataService', () => { it('should include nested linksToFollow 3lvl', () => { const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`; - const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); + const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))); expect(result).toEqual(expected); }); }); diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts index b26e561b43..9fb5135d34 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -14,9 +14,10 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from './data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RemoteData } from './remote-data'; -import { FindByIDRequest, IdentifierType } from './request.models'; +import { IdentifierType } from './request.models'; import { RequestService } from './request.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; +import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class DsoRedirectDataService extends DataService { @@ -53,7 +54,7 @@ export class DsoRedirectDataService extends DataService { {}, [], ...linksToFollow); } - findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable> { + findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable> { this.setLinkPath(identifierType); return this.findById(id).pipe( getFirstCompletedRemoteData(), diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index 4f62af3f6f..4b3fafa73a 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -3,7 +3,7 @@ import { TestScheduler } from 'rxjs/testing'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindByIDRequest } from './request.models'; +import { GetRequest } from './request.models'; import { RequestService } from './request.service'; import { DSpaceObjectDataService } from './dspace-object-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -32,7 +32,7 @@ describe('DSpaceObjectDataService', () => { }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - configure: true + send: true }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { @@ -65,11 +65,11 @@ describe('DSpaceObjectDataService', () => { expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); }); - it('should configure the proper FindByIDRequest', () => { + it('should send the proper FindByIDRequest', () => { scheduler.schedule(() => service.findById(testObject.uuid)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid)); + expect(requestService.send).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL), true); }); it('should return a RemoteData for the object with the given ID', () => { diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 716c302de1..eb230e2f54 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -61,37 +61,46 @@ export class DSpaceObjectDataService { * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the object * @param id ID of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findById(id: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findById(id, reRequestOnStale, ...linksToFollow); + findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the object - * @param href The url of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param href The url of object we want to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); + findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the object - * @param href The url of object we want to retrieve - * @param findListOptions Find list options object - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param href The url of object we want to retrieve + * @param findListOptions Find list options object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); + findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } } diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts deleted file mode 100644 index 3acf19c7fc..0000000000 --- a/src/app/core/data/entity-type-data.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../cache/builders/build-decorators'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { CoreState } from '../core.reducers'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ItemType } from '../shared/item-relationships/item-type.model'; -import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; -import { DataService } from './data.service'; -import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { FindListOptions } from './request.models'; -import { RequestService } from './request.service'; - -/* tslint:disable:max-classes-per-file */ - -/** - * A private DataService implementation to delegate specific methods to. - */ -class DataServiceImpl extends DataService { - protected linkPath = 'entitytypes'; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - super(); - } -} - -/** - * A service to retrieve {@link ItemType}s from the REST API. - */ -@Injectable() -@dataService(ITEM_TYPE) -export class ItemTypeDataService { - /** - * A private DataService instance to delegate specific methods to. - */ - private dataService: DataServiceImpl; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); - } - - /** - * Returns an observable of {@link RemoteData} of an {@link ItemType}, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link ItemType} - * @param href The url of {@link ItemType} we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); - } - - /** - * Returns a list of observables of {@link RemoteData} of {@link ItemType}s, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link ItemType} - * @param href The url of the {@link ItemType} we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); - } -} -/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/entity-type.service.ts b/src/app/core/data/entity-type.service.ts index 3868fb9eff..ca9ea15bc6 100644 --- a/src/app/core/data/entity-type.service.ts +++ b/src/app/core/data/entity-type.service.ts @@ -10,14 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { Injectable } from '@angular/core'; -import { GetRequest } from './request.models'; import { Observable } from 'rxjs'; import { switchMap, take, map } from 'rxjs/operators'; import { RemoteData } from './remote-data'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import {PaginatedList} from './paginated-list.model'; +import { PaginatedList } from './paginated-list.model'; import { ItemType } from '../shared/item-relationships/item-type.model'; -import {getRemoteDataPayload, getFirstSucceededRemoteData} from '../shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../shared/operators'; +import { RelationshipTypeService } from './relationship-type.service'; /** * Service handling all ItemType requests @@ -33,6 +33,7 @@ export class EntityTypeService extends DataService { protected halService: HALEndpointService, protected objectCache: ObjectCacheService, protected notificationsService: NotificationsService, + protected relationshipTypeService: RelationshipTypeService, protected http: HttpClient, protected comparator: DefaultChangeAnalyzer) { super(); @@ -69,18 +70,16 @@ export class EntityTypeService extends DataService { /** * Get the allowed relationship types for an entity type * @param entityTypeId - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - getEntityTypeRelationships(entityTypeId: string, ...linksToFollow: FollowLinkConfig[]): Observable>> { - + getEntityTypeRelationships(entityTypeId: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const href$ = this.getRelationshipTypesEndpoint(entityTypeId); - - href$.pipe(take(1)).subscribe((href) => { - const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); - }); - - return this.rdbService.buildList(href$, ...linksToFollow); + return this.relationshipTypeService.findAllByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 04cf24dd02..fe3a1958eb 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -31,7 +31,7 @@ describe('EpersonRegistrationService', () => { requestService = jasmine.createSpyObj('requestService', { generateRequestId: 'request-id', - configure: {}, + send: {}, getByUUID: cold('a', { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }) }); @@ -70,7 +70,7 @@ describe('EpersonRegistrationService', () => { const expected = service.registerEmail('test@mail.org'); - expect(requestService.configure).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); + expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); expect(expected).toBeObservable(cold('(a|)', { a: rd })); }); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 15bdced8d0..fd55c031d8 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -66,7 +66,7 @@ export class EpersonRegistrationService { find((href: string) => hasValue(href)), map((href: string) => { const request = new PostRequest(requestId, href, registration); - this.requestService.configure(request); + this.requestService.send(request); }) ).subscribe(); @@ -93,7 +93,7 @@ export class EpersonRegistrationService { return RegistrationResponseParsingService; } }); - this.requestService.configure(request); + this.requestService.send(request, true); }) ).subscribe(); diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts index 7c7d676bc7..59226197d1 100644 --- a/src/app/core/data/external-source.service.spec.ts +++ b/src/app/core/data/external-source.service.spec.ts @@ -42,7 +42,7 @@ describe('ExternalSourceService', () => { function init() { requestService = jasmine.createSpyObj('requestService', { generateRequestId: 'request-uuid', - configure: {} + send: {} }); rdbService = jasmine.createSpyObj('rdbService', { buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) @@ -64,8 +64,8 @@ describe('ExternalSourceService', () => { result = service.getExternalSourceEntries('test'); }); - it('should configure a GetRequest', () => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + it('should send a GetRequest', () => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true); }); it('should return the entries', () => { diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index 3337da1c28..a3a0a532ec 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -9,16 +9,16 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { FindListOptions, GetRequest } from './request.models'; +import { FindListOptions } from './request.models'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; +import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; -import { configureRequest } from '../shared/operators'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * A service handling all external source requests @@ -61,23 +61,24 @@ export class ExternalSourceService extends DataService { /** * Get the entries for an external source - * @param externalSourceId The id of the external source to fetch entries for - * @param searchOptions The search options to limit results to + * @param externalSourceId The id of the external source to fetch entries for + * @param searchOptions The search options to limit results to + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable>> { - const requestUuid = this.requestService.generateRequestId(); - + getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const href$ = this.getEntriesEndpoint(externalSourceId).pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) + map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint), + take(1) ); - href$.pipe( - map((endpoint: string) => new GetRequest(requestUuid, endpoint)), - configureRequest(this.requestService) - ).subscribe(); - - return this.rdbService.buildList(href$); + // TODO create a dedicated ExternalSourceEntryDataService and move this entire method to it. Then the "as any"s won't be necessary + return this.findAllByHref(href$, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as any) as any; } } diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts index 951768577b..23457b8409 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts @@ -68,7 +68,7 @@ describe('AuthorizationDataService', () => { }); it('should call searchBy with the site\'s url', () => { - expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self), true); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self), true, true); }); }); @@ -78,7 +78,7 @@ describe('AuthorizationDataService', () => { }); it('should call searchBy with the site\'s url and the feature', () => { - expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf), true); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf), true, true); }); }); @@ -88,7 +88,7 @@ describe('AuthorizationDataService', () => { }); it('should call searchBy with the object\'s url and the feature', () => { - expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf), true); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf), true, true); }); }); @@ -98,7 +98,7 @@ describe('AuthorizationDataService', () => { }); it('should call searchBy with the object\'s url, user\'s uuid and the feature', () => { - expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true, true); }); }); }); diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index 9a247b32bf..170e82f5f8 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -60,7 +60,7 @@ export class AuthorizationDataService extends DataService { * @param featureId ID of the {@link Feature} to check {@link Authorization} for */ isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable { - return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, followLink('feature')).pipe( + return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, true, true, followLink('feature')).pipe( getFirstCompletedRemoteData(), map((authorizationRD) => { if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) { @@ -77,19 +77,24 @@ export class AuthorizationDataService extends DataService { /** * Search for a list of {@link Authorization}s using the "object" search endpoint and providing optional object url, * {@link EPerson} uuid and/or {@link Feature} id - * @param objectUrl URL to the object to search {@link Authorization}s for. - * If not provided, the repository's {@link Site} will be used. - * @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for. - * If not provided, the UUID of the currently authenticated {@link EPerson} will be used. - * @param featureId ID of the {@link Feature} to search {@link Authorization}s for - * @param options {@link FindListOptions} to provide pagination and/or additional arguments - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param objectUrl URL to the object to search {@link Authorization}s for. + * If not provided, the repository's {@link Site} will be used. + * @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for. + * If not provided, the UUID of the currently authenticated {@link EPerson} will be used. + * @param featureId ID of the {@link Feature} to search {@link Authorization}s for + * @param options {@link FindListOptions} to provide pagination and/or additional arguments + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { + searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe( addSiteObjectUrlIfEmpty(this.siteService), switchMap((params: AuthorizationSearchParams) => { - return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), true, ...linksToFollow); + return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); }) ); } diff --git a/src/app/core/data/href-only-data.service.spec.ts b/src/app/core/data/href-only-data.service.spec.ts new file mode 100644 index 0000000000..dd4be83203 --- /dev/null +++ b/src/app/core/data/href-only-data.service.spec.ts @@ -0,0 +1,82 @@ +import { HrefOnlyDataService } from './href-only-data.service'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { FindListOptions } from './request.models'; +import { DataService } from './data.service'; + +describe(`HrefOnlyDataService`, () => { + let service: HrefOnlyDataService; + let href: string; + let spy: jasmine.Spy; + let followLinks: FollowLinkConfig[]; + let findListOptions: FindListOptions; + + beforeEach(() => { + href = 'https://rest.api/server/api/core/items/de7fa215-4a25-43a7-a4d7-17534a09fdfc'; + followLinks = [ followLink('link1'), followLink('link2') ]; + findListOptions = new FindListOptions(); + service = new HrefOnlyDataService(null, null, null, null, null, null, null, null); + }); + + it(`should instantiate a private DataService`, () => { + expect((service as any).dataService).toBeDefined(); + expect((service as any).dataService).toBeInstanceOf(DataService); + }); + + describe(`findByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); + + it(`should delegate to findByHref on the internal DataService`, () => { + service.findByHref(href, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks); + }); + + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything()); + }); + }); + + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true); + }); + }); + }); + + describe(`findAllByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findAllByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); + + it(`should delegate to findAllByHref on the internal DataService`, () => { + service.findAllByHref(href, findListOptions, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks); + }); + + describe(`when findListOptions is omitted`, () => { + it(`should call findAllByHref on the internal DataService with findListOptions = {}`, () => { + service.findAllByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything()); + }); + }); + + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findAllByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findAllByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything()); + }); + }); + + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findAllByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findAllByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true); + }); + }); + }); +}); diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts new file mode 100644 index 0000000000..c1298c054c --- /dev/null +++ b/src/app/core/data/href-only-data.service.ts @@ -0,0 +1,99 @@ +import { DataService } from './data.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { Injectable } from '@angular/core'; +import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type'; +import { FindListOptions } from './request.models'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteData } from './remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from './paginated-list.model'; +import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; +import { LICENSE } from '../shared/license.resource-type'; +import { CacheableObject } from '../cache/object-cache.reducer'; + +/* tslint:disable:max-classes-per-file */ +class DataServiceImpl extends DataService { + // linkPath isn't used if we're only searching by href. + protected linkPath = undefined; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +/** + * A DataService with only findByHref methods. Its purpose is to be used for resources that don't + * need to be retrieved by ID, or have any way to update them, but require a DataService in order + * for their links to be resolved by the LinkService. + * + * an @dataService annotation can be added for any number of these resource types + */ +@Injectable({ + providedIn: 'root' +}) +@dataService(VOCABULARY_ENTRY) +@dataService(ITEM_TYPE) +@dataService(LICENSE) +export class HrefOnlyDataService { + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, store, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + */ + findByHref(href: string | Observable, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param findListOptions Find list options object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + */ + findAllByHref(href: string | Observable, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } +} diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 5b7278632a..30a132aeae 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -126,8 +126,8 @@ describe('ItemDataService', () => { result = service.removeMappingFromCollection('item-id', 'collection-id'); }); - it('should configure a DELETE request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest))); + it('should send a DELETE request', () => { + result.subscribe(() => expect(requestService.send).toHaveBeenCalledWith(jasmine.any(DeleteRequest))); }); }); @@ -139,8 +139,8 @@ describe('ItemDataService', () => { result = service.mapToCollection('item-id', 'collection-href'); }); - it('should configure a POST request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); + it('should send a POST request', () => { + result.subscribe(() => expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest))); }); }); @@ -158,9 +158,9 @@ describe('ItemDataService', () => { result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id'); }); - it('should configure a POST request', (done) => { + it('should send a POST request', (done) => { result.subscribe(() => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest)); done(); }); }); @@ -176,9 +176,9 @@ describe('ItemDataService', () => { result = service.createBundle(itemId, bundleName); }); - it('should configure a POST request', (done) => { + it('should send a POST request', (done) => { result.subscribe(() => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PostRequest)); done(); }); }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 8cab07d136..e56f9f2b0c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -16,7 +16,7 @@ import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { ITEM } from '../shared/item.resource-type'; -import { configureRequest } from '../shared/operators'; +import { sendRequest } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; @@ -99,7 +99,7 @@ export class ItemDataService extends DataService { isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), - configureRequest(this.requestService), + sendRequest(this.requestService), switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), ); } @@ -120,29 +120,11 @@ export class ItemDataService extends DataService { options.headers = headers; return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); }), - configureRequest(this.requestService), + sendRequest(this.requestService), switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)) ); } - /** - * Fetches all collections the item is mapped to - * @param itemId The item's id - */ - public getMappedCollections(itemId: string): Observable>> { - const href$ = this.getMappedCollectionsEndpoint(itemId).pipe( - isNotEmptyOperator(), - take(1) - ); - - href$.subscribe((href: string) => { - const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); - }); - - return this.rdbService.buildList(href$); - } - /** * Set the isWithdrawn state of an item to a specified state * @param item @@ -196,7 +178,7 @@ export class ItemDataService extends DataService { take(1) ).subscribe((href) => { const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); + this.requestService.send(request); }); return this.rdbService.buildList(hrefObs); @@ -225,7 +207,7 @@ export class ItemDataService extends DataService { headers = headers.append('Content-Type', 'application/json'); options.headers = headers; const request = new PostRequest(requestId, href, JSON.stringify(bundleJson), options); - this.requestService.configure(request); + this.requestService.send(request); }); return this.rdbService.buildFromRequestUUID(requestId); @@ -260,7 +242,7 @@ export class ItemDataService extends DataService { find((href: string) => hasValue(href)), map((href: string) => { const request = new PutRequest(requestId, href, collection._links.self.href, options); - this.requestService.configure(request); + this.requestService.send(request); }) ).subscribe(); @@ -285,7 +267,7 @@ export class ItemDataService extends DataService { find((href: string) => hasValue(href)), map((href: string) => { const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options); - this.requestService.configure(request); + this.requestService.send(request); }) ).subscribe(); diff --git a/src/app/core/data/item-template-data.service.spec.ts b/src/app/core/data/item-template-data.service.spec.ts index 8a9744567c..1458527506 100644 --- a/src/app/core/data/item-template-data.service.spec.ts +++ b/src/app/core/data/item-template-data.service.spec.ts @@ -28,7 +28,7 @@ describe('ItemTemplateDataService', () => { generateRequestId(): string { return scopeID; }, - configure(request: RestRequest) { + send(request: RestRequest) { // Do nothing }, getByHref(requestHref: string) { diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index 3b4125f4e7..64f22d8aa5 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -100,13 +100,16 @@ class DataServiceImpl extends ItemDataService { /** * Set the collection ID and send a find by ID request * @param collectionID - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { this.setCollectionEndpoint(collectionID); - return super.findById(collectionID, reRequestOnStale, ...linksToFollow); + return super.findById(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -178,12 +181,15 @@ export class ItemTemplateDataService implements UpdateDataService { /** * Find an item template by collection ID * @param collectionID - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByCollectionID(collectionID, reRequestOnStale, ...linksToFollow); + findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByCollectionID(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/data/license-data.service.ts b/src/app/core/data/license-data.service.ts deleted file mode 100644 index 4eccf68674..0000000000 --- a/src/app/core/data/license-data.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../cache/builders/build-decorators'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { CoreState } from '../core.reducers'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { License } from '../shared/license.model'; -import { LICENSE } from '../shared/license.resource-type'; -import { DataService } from './data.service'; -import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { FindListOptions } from './request.models'; -import { RequestService } from './request.service'; - -/* tslint:disable:max-classes-per-file */ - -/** - * A private DataService implementation to delegate specific methods to. - */ -class DataServiceImpl extends DataService { - protected linkPath = ''; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - super(); - } -} - -/** - * A service to retrieve {@link License}s from the REST API. - */ -@Injectable() -@dataService(LICENSE) -export class LicenseDataService { - /** - * A private DataService instance to delegate specific methods to. - */ - private dataService: DataServiceImpl; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); - } - - /** - * Returns an observable of {@link RemoteData} of a {@link License}, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link License} - * @param href The URL of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); - } - - /** - * Returns a list of observables of {@link RemoteData} of {@link License}s, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link License} - * @param href The URL of object we want to retrieve - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); - } -} -/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/metadata-field-data.service.spec.ts b/src/app/core/data/metadata-field-data.service.spec.ts index c2d1f28d8f..bb621f74b3 100644 --- a/src/app/core/data/metadata-field-data.service.spec.ts +++ b/src/app/core/data/metadata-field-data.service.spec.ts @@ -31,9 +31,9 @@ describe('MetadataFieldDataService', () => { }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', - configure: {}, + send: {}, getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), - removeByHrefSubstring: {} + setStaleByHrefSubstring: {} }); halService = Object.assign(new HALEndpointServiceStub(endpoint)); notificationsService = jasmine.createSpyObj('notificationsService', { @@ -59,16 +59,15 @@ describe('MetadataFieldDataService', () => { const expectedOptions = Object.assign(new FindListOptions(), { searchParams: [new RequestParam('schema', schema.prefix)] }); - expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true); + expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true, true); }); }); describe('clearRequests', () => { - it('should remove requests on the data service\'s endpoint', (done) => { - metadataFieldService.clearRequests().subscribe(() => { - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`); - done(); - }); + it('should remove requests on the data service\'s endpoint', () => { + spyOn(metadataFieldService, 'getBrowseEndpoint').and.returnValue(observableOf(endpoint)); + metadataFieldService.clearRequests(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(endpoint); }); }); }); diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index 0ba4263777..3b11859361 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -19,7 +19,7 @@ import { MetadataSchema } from '../metadata/metadata-schema.model'; import { FindListOptions } from './request.models'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { RequestParam } from '../cache/models/request-param.model'; /** @@ -46,17 +46,20 @@ export class MetadataFieldDataService extends DataService { /** * Find metadata fields belonging to a metadata schema - * @param schema The metadata schema to list fields for - * @param options The options info used to retrieve the fields - * @param reRequestOnStale Whether or not the request should automatically be re-requested after - * the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param schema The metadata schema to list fields for + * @param options The options info used to retrieve the fields + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findBySchema(schema: MetadataSchema, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { + findBySchema(schema: MetadataSchema, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { const optionsWithSchema = Object.assign(new FindListOptions(), options, { searchParams: [new RequestParam('schema', schema.prefix)] }); - return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, reRequestOnStale, ...linksToFollow); + return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -71,10 +74,11 @@ export class MetadataFieldDataService extends DataService { * schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value * if there's an exact match * @param options The options info used to retrieve the fields + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's no valid cached version. Defaults to true * @param reRequestOnStale Whether or not the request should automatically be re-requested after the response becomes stale * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const optionParams = Object.assign(new FindListOptions(), options, { searchParams: [ new RequestParam('schema', hasValue(schema) ? schema : ''), @@ -84,7 +88,7 @@ export class MetadataFieldDataService extends DataService { new RequestParam('exactName', hasValue(exactName) ? exactName : '') ] }); - return this.searchBy(this.searchByFieldNameLinkPath, optionParams, reRequestOnStale, ...linksToFollow); + return this.searchBy(this.searchByFieldNameLinkPath, optionParams, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -101,12 +105,11 @@ export class MetadataFieldDataService extends DataService { * Clear all metadata field requests * Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema */ - clearRequests(): Observable { - return this.getBrowseEndpoint().pipe( - tap((href: string) => { - this.requestService.removeByHrefSubstring(href); - }) - ); + clearRequests(): void { + this.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => { + this.requestService.setStaleByHrefSubstring(href); + }); + } } diff --git a/src/app/core/data/metadata-schema-data.service.spec.ts b/src/app/core/data/metadata-schema-data.service.spec.ts index f8c972bc04..2e61955502 100644 --- a/src/app/core/data/metadata-schema-data.service.spec.ts +++ b/src/app/core/data/metadata-schema-data.service.spec.ts @@ -22,7 +22,7 @@ describe('MetadataSchemaDataService', () => { function init() { requestService = jasmine.createSpyObj('requestService', { generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', - configure: {}, + send: {}, getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), removeByHrefSubstring: {} }); @@ -54,7 +54,7 @@ describe('MetadataSchemaDataService', () => { describe('called with a new metadata schema', () => { it('should send a CreateRequest', (done) => { metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(CreateRequest)); done(); }); }); @@ -69,7 +69,7 @@ describe('MetadataSchemaDataService', () => { it('should send a PutRequest', (done) => { metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { - expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest)); done(); }); }); diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 6d30faa73e..cadcdb3bfe 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -13,12 +13,11 @@ import { Process } from '../../../process-page/processes/process.model'; import { dataService } from '../../cache/builders/build-decorators'; import { PROCESS } from '../../../process-page/processes/process.resource-type'; import { Observable } from 'rxjs'; -import { switchMap, take } from 'rxjs/operators'; -import { GetRequest } from '../request.models'; +import { switchMap } from 'rxjs/operators'; import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; import { RemoteData } from '../remote-data'; -import { isNotEmptyOperator } from '../../../shared/empty.util'; +import { BitstreamDataService } from '../bitstream-data.service'; @Injectable() @dataService(PROCESS) @@ -32,6 +31,7 @@ export class ProcessDataService extends DataService { protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, + protected bitstreamDataService: BitstreamDataService, protected http: HttpClient, protected comparator: DefaultChangeAnalyzer) { super(); @@ -48,20 +48,11 @@ export class ProcessDataService extends DataService { } /** - * Get a process his output files + * Get a process' output files * @param processId The ID of the process */ getFiles(processId: string): Observable>> { - const href$ = this.getFilesEndpoint(processId).pipe( - isNotEmptyOperator(), - take(1) - ); - - href$.subscribe((href: string) => { - const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); - }); - - return this.rdbService.buildList(href$); + const href$ = this.getFilesEndpoint(processId); + return this.bitstreamDataService.findAllByHref(href$); } } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index eab20b370e..69b4270173 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -49,7 +49,7 @@ export class ScriptDataService extends DataService