Merge branch 'main' into CST-3620

This commit is contained in:
Alessandro Martelli
2021-02-09 11:23:24 +01:00
190 changed files with 8775 additions and 4544 deletions

View File

@@ -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'),

View File

@@ -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');
}
}

View File

@@ -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<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject<RemoteData<PaginatedList<EPerson>>>({} as any);
ePeople$: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject(buildPaginatedList<EPerson>(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<EPerson>) => {
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<EpersonDtoModel>) => {
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;
});
});
}
}

View File

@@ -359,7 +359,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
});
const response = this.epersonService.updateEPerson(editedEperson);
response.pipe(take(1)).subscribe((rd: RemoteData<EPerson>) => {
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
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<NoContent>) => {
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
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);
}

View File

@@ -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())

View File

@@ -24,10 +24,10 @@
</div>
</form>
<ds-pagination *ngIf="(ePeopleSearch | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
[paginationOptions]="configSearch"
[pageInfoState]="(ePeopleSearch | async)?.payload"
[collectionSize]="(ePeopleSearch | async)?.payload?.totalElements"
[pageInfoState]="(searchResults$ | async)?.payload"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChangeSearch($event)">
@@ -42,7 +42,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (ePeopleSearch | async)?.payload?.page">
<tr *ngFor="let ePerson of (searchResults$ | async)?.payload?.page">
<td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
@@ -70,7 +70,7 @@
</ds-pagination>
<div *ngIf="(ePeopleSearch | async)?.payload?.totalElements == 0 && searchDone"
<div *ngIf="(searchResults$ | async)?.payload?.totalElements == 0 && searchDone"
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
@@ -78,10 +78,10 @@
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(members$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeopleMembersOfGroup | async)?.payload"
[collectionSize]="(ePeopleMembersOfGroup | async)?.payload?.totalElements"
[pageInfoState]="(members$ | async)?.payload"
[collectionSize]="(members$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
@@ -96,7 +96,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (ePeopleMembersOfGroup | async)?.payload?.page">
<tr *ngFor="let ePerson of (members$ | async)?.payload?.page">
<td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
@@ -116,7 +116,7 @@
</ds-pagination>
<div *ngIf="(ePeopleMembersOfGroup | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
<div *ngIf="(members$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>

View File

@@ -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';
@@ -15,10 +15,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';
/**
* Keys to keep track of specific subscriptions
*/
enum SubKey {
Members,
ActiveGroup,
SearchResults,
}
@Component({
selector: 'ds-members-list',
templateUrl: './members-list.component.html'
@@ -34,11 +42,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* EPeople being displayed in search result, initially all members, after search result of search
*/
ePeopleSearch: Observable<RemoteData<PaginatedList<EPerson>>>;
searchResults$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
/**
* List of EPeople members of currently active group being edited
*/
ePeopleMembersOfGroup: Observable<RemoteData<PaginatedList<EPerson>>>;
members$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
/**
* Pagination config used to display the list of EPeople that are result of EPeople search
@@ -58,9 +66,9 @@ export class MembersListComponent implements OnInit, OnDestroy {
});
/**
* List of subscriptions
* Map of active subscriptions
*/
subs: Subscription[] = [];
subs: Map<SubKey, Subscription> = new Map();
// The search form
searchForm;
@@ -90,10 +98,10 @@ export class MembersListComponent implements OnInit, OnDestroy {
scope: 'metadata',
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.forceUpdateEPeople(activeGroup);
this.retrieveMembers(this.config.currentPage);
}
}));
}
@@ -112,10 +120,40 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @param event
*/
onPageChange(event) {
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: event,
elementsPerPage: this.config.pageSize
});
this.retrieveMembers(event);
}
/**
* Retrieve the EPersons that are members of the group
*
* @param page the number of the page to retrieve
* @private
*/
private retrieveMembers(page: number) {
this.unsubFrom(SubKey.Members);
this.subs.set(
SubKey.Members,
this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: page,
elementsPerPage: this.config.pageSize
}
).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
this.members$.next(rd);
}));
}
/**
* 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
*/
private unsubFrom(key: SubKey) {
if (this.subs.has(key)) {
this.subs.get(key).unsubscribe();
this.subs.delete(key);
}
}
/**
@@ -127,7 +165,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
if (activeGroup != null) {
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson);
this.showNotifications('deleteMember', response, ePerson.name, activeGroup);
this.forceUpdateEPeople(activeGroup);
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
@@ -147,7 +184,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
});
this.forceUpdateEPeople(this.groupBeingEdited, ePerson);
}
/**
@@ -159,8 +195,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
mergeMap((group: Group) => {
if (group != null) {
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
currentPage: 0,
elementsPerPage: Number.MAX_SAFE_INTEGER
currentPage: 1,
elementsPerPage: 9999
})
.pipe(
getFirstSucceededRemoteData(),
@@ -191,34 +227,23 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.configSearch.currentPage = 1;
}
this.searchDone = true;
this.ePeopleSearch = this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize
});
}
/**
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
* a new REST call
* @param activeGroup Group currently being edited
*/
public forceUpdateEPeople(activeGroup: Group, ePersonToUpdate?: EPerson) {
if (ePersonToUpdate != null) {
this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href);
}
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, {
this.unsubFrom(SubKey.SearchResults);
this.subs.set(SubKey.SearchResults, this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize
});
}).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
this.searchResults$.next(rd);
}));
}
/**
* 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);
}
}
/**
@@ -232,6 +257,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
}

View File

@@ -18,10 +18,10 @@
</div>
</form>
<ds-pagination *ngIf="(groupsSearch | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
[paginationOptions]="configSearch"
[pageInfoState]="(groupsSearch | async)?.payload"
[collectionSize]="(groupsSearch | async)?.payload?.totalElements"
[pageInfoState]="(searchResults$ | async)?.payload"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChangeSearch($event)">
@@ -36,7 +36,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (groupsSearch | async)?.payload?.page">
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
<td>{{group.id}}</td>
<td><a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
@@ -65,17 +65,17 @@
</div>
</ds-pagination>
<div *ngIf="(groupsSearch | async)?.payload?.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
<div *ngIf="(searchResults$ | async)?.payload?.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subgroupsOfGroup | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(subgroupsOfGroup | async)?.payload"
[collectionSize]="(subgroupsOfGroup | async)?.payload?.totalElements"
[pageInfoState]="(subGroups$ | async)?.payload"
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
@@ -90,7 +90,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (subgroupsOfGroup | async)?.payload?.page">
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
<td>{{group.id}}</td>
<td><a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
@@ -109,7 +109,7 @@
</div>
</ds-pagination>
<div *ngIf="(subgroupsOfGroup | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
</div>

View File

@@ -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<Group> {
return observableOf(this.activeGroup);
},
@@ -51,7 +63,11 @@ describe('SubgroupsListComponent', () => {
return this.activeGroup;
},
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList<Group>(new PageInfo(), this.subgroups));
return this.subgroups$.pipe(
map((currentGroups: Group[]) => {
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(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<RestResponse> {
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<RestResponse> {
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'));
}
};

View File

@@ -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<RemoteData<PaginatedList<Group>>>;
searchResults$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject(undefined);
/**
* List of all subgroups of group being edited
*/
subgroupsOfGroup: Observable<RemoteData<PaginatedList<Group>>>;
subGroups$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject(undefined);
/**
* List of subscriptions
* Map of active subscriptions
*/
subs: Subscription[] = [];
subs: Map<SubKey, Subscription> = 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<PaginatedList<Group>>) => {
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<PaginatedList<Group>>) => {
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<RemoteData<Group>>, nameObject: string, activeGroup: Group) {
showNotifications(messageSuffix: string, response: Observable<RemoteData<Group|NoContent>>, nameObject: string, activeGroup: Group) {
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<Group>) => {
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 }));
}

View File

@@ -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<Group>) => {
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<GroupDtoModel>) => {
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<PaginatedList<Group>>) => {
this.groups$.next(groupsRD);
this.pageInfoState$.next(groupsRD.payload.pageInfo);
}
));
this.subs.push(this.groups$.pipe(
getAllSucceededRemoteDataPayload(),
switchMap((groups: PaginatedList<Group>) => {
return observableCombineLatest(...groups.page.map((group: Group) => {
return observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
this.hasLinkedDSO(group),
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<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<GroupDtoModel>) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
}));
}).pipe(
getAllSucceededRemoteData()
).subscribe((groupsRD: RemoteData<PaginatedList<Group>>) => {
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);
});
}

View File

@@ -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 = {

View File

@@ -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<NoContent>) => response.hasFailed);
if (successResponses.length > 0) {
this.showNotification(true, successResponses.length);
this.registryService.clearMetadataFieldRequests();
}
if (failedResponses.length > 0) {
this.showNotification(false, failedResponses.length);

View File

@@ -23,7 +23,7 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> {
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<RemoteData<Bitstream>> {
*/
get followLinks(): FollowLinkConfig<Bitstream>[] {
return [
followLink('bundle', undefined, true, followLink('item')),
followLink('bundle', undefined, true, true, true, followLink('item')),
followLink('format')
];
}

View File

@@ -2,7 +2,7 @@
<div class="row">
<div class="col-12">
<h2>{{'collection.edit.item-mapper.head' | translate}}</h2>
<p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionRD$ | async)?.payload?.name }" id="collection-name"></p>
<p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionName$ |async) }" id="collection-name"></p>
<p>{{'collection.edit.item-mapper.description' | translate}}</p>
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">

View File

@@ -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<Collection> = 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 */

View File

@@ -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<RemoteData<Collection>>;
collectionName$: Observable<string>;
/**
* 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<RemoteData<Collection>>;
this.collectionRD$ = this.route.parent.data.pipe(
map((data) => data.dso as RemoteData<Collection>),
getFirstSucceededRemoteData()
);
this.collectionName$ = this.collectionRD$.pipe(
map((rd: RemoteData<Collection>) => {
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<Collection>) => 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();
});
}

View File

@@ -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: {

View File

@@ -23,7 +23,7 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id, false, followLink('logo')).pipe(
return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe(
getFirstCompletedRemoteData()
);
}

View File

@@ -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 }
},
]
}
])

View File

@@ -23,7 +23,7 @@ export class ItemTemplatePageResolver implements Resolve<RemoteData<Item>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemTemplateService.findByCollectionID(route.params.id, false, followLink('templateItemOf')).pipe(
return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
getFirstCompletedRemoteData(),
);
}

View File

@@ -25,6 +25,7 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById(
route.params.id,
true,
false,
followLink('logo'),
followLink('subcommunities'),

View File

@@ -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

View File

@@ -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<FieldUpdates>;
/**
* 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<Item>) => 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<Item>) => {
this.item = rd.payload;
}),
switchMap((rd: RemoteData<Item>) => {
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
}),
getAllSucceededRemoteData()
).subscribe((rd: RemoteData<Item>) => {
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

View File

@@ -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 }
}
]
},

View File

@@ -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<Item>;

View File

@@ -134,6 +134,7 @@ describe('ItemBitstreamsComponent', () => {
});
itemService = Object.assign({
getBitstreams: () => createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])),
findByHref: () => createSuccessfulRemoteDataObject$(item),
findById: () => createSuccessfulRemoteDataObject$(item),
getBundles: () => createSuccessfulRemoteDataObject$(createPaginatedList([bundle]))
});

View File

@@ -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<Item>) => {
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();
}
/**

View File

@@ -2,7 +2,7 @@
<div class="row">
<div class="col-12">
<h2>{{'item.edit.item-mapper.head' | translate}}</h2>
<p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemRD$ | async)?.payload?.name }" id="item-name"></p>
<p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemName$ | async) }" id="item-name"></p>
<p>{{'item.edit.item-mapper.description' | translate}}</p>
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">

View File

@@ -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<Item> = 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();
}));

View File

@@ -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<RemoteData<Item>>;
itemName$: Observable<string>;
/**
* 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<RemoteData<Item>>;
this.itemRD$ = this.route.parent.data.pipe(
take(1),
map((data) => data.dso),
);
this.itemName$ = this.itemRD$.pipe(
filter((rd: RemoteData<Item>) => hasValue(rd)),
map((rd: RemoteData<Item>) => {
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<boolean>(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<Item>) => rd.payload),
map((item: Item) => item.id)
),
this.itemCollectionsRD$.pipe(
getFirstSucceededRemoteData(),
take(1),
map((rd: RemoteData<PaginatedList<Collection>>) => 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<Item>) => 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();
});
}

View File

@@ -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', () => {

View File

@@ -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[]) => {

View File

@@ -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'
}
}
},
{

View File

@@ -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'),

View File

@@ -133,6 +133,7 @@ describe('ItemRelationshipsComponent', () => {
};
itemService = jasmine.createSpyObj('itemService', {
findByHref: createSuccessfulRemoteDataObject$(item),
findById: createSuccessfulRemoteDataObject$(item)
});
routeStub = {

View File

@@ -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<RemoteData<Item>>;
/**
* 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[]) => {

View File

@@ -67,6 +67,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
'ORIGINAL',
{elementsPerPage: this.pageSize, currentPage: pageNumber},
true,
true,
followLink('format')
)),
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
@@ -83,6 +84,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
'LICENSE',
{elementsPerPage: this.pageSize, currentPage: pageNumber},
true,
true,
followLink('format')
)),
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {

View File

@@ -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<Item>[] = [
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<RemoteData<Item>> {
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
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(),
);

View File

@@ -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<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(

View File

@@ -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<boolean> {
const params = this.getLookupParams(route);
return this.dsoService.findByIdAndIDType(params.id, params.type).pipe(
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
map((response: RemoteData<DSpaceObject>) => response.hasFailed)
);
}

View File

@@ -24,6 +24,7 @@ export class WorkflowItemPageResolver implements Resolve<RemoteData<WorkflowItem
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<WorkflowItem>> {
return this.workflowItemService.findById(route.params.id,
true,
false,
followLink('item'),
).pipe(

View File

@@ -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,
];

View File

@@ -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<ShortLivedToken>(request.uuid)),
getFirstCompletedRemoteData(),
map((response: RemoteData<ShortLivedToken>) => {

View File

@@ -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<AuthStatus>) => {
const status = rd.payload;
if (hasValue(status) && !status.authenticated) {

View File

@@ -23,7 +23,7 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collecti
*/
get followLinks(): FollowLinkConfig<Collection>[] {
return [
followLink('parentCommunity', undefined, true,
followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity')
)
];

View File

@@ -29,7 +29,7 @@ export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceO
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
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) => {

View File

@@ -23,8 +23,8 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
*/
get followLinks(): FollowLinkConfig<Item>[] {
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'),

View File

@@ -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);
});
});

View File

@@ -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<RemoteData<PaginatedList<BrowseDefinition>>>}
* Return an observable that emits object list
*/
findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAll(options, reRequestOnStale, ...linksToFollow);
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
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<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow);
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<BrowseDefinition>> {
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<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -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
}));
});
});

View File

@@ -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<RemoteData<PaginatedList<BrowseEntry>>> {
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<BrowseEntry>(href$);
}
/**
@@ -105,7 +106,7 @@ export class BrowseService {
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
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<Item>(href$);
}
/**
@@ -146,7 +147,7 @@ export class BrowseService {
* @param scope
*/
getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> {
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<Item>(href$).pipe(
getFirstSucceededRemoteData(),
getFirstOccurrence()
);
}
/**
@@ -177,9 +181,7 @@ export class BrowseService {
* @param items
*/
getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
return observableOf(items.payload.prev).pipe(
getBrowseItemsFor(this.requestService, this.rdb)
);
return this.hrefOnlyDataService.findAllByHref<Item>(items.payload.prev);
}
/**
@@ -187,9 +189,7 @@ export class BrowseService {
* @param items
*/
getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
return observableOf(items.payload.next).pipe(
getBrowseItemsFor(this.requestService, this.rdb)
);
return this.hrefOnlyDataService.findAllByHref<Item>(items.payload.next);
}
/**
@@ -197,9 +197,7 @@ export class BrowseService {
* @param entries
*/
getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
return observableOf(entries.payload.prev).pipe(
getBrowseEntriesFor(this.requestService, this.rdb)
);
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(entries.payload.prev);
}
/**
@@ -207,9 +205,7 @@ export class BrowseService {
* @param entries
*/
getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
return observableOf(entries.payload.next).pipe(
getBrowseEntriesFor(this.requestService, this.rdb)
);
return this.hrefOnlyDataService.findAllByHref<BrowseEntry>(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<string>): Observable<RemoteData<PaginatedList<BrowseEntry>>> => {
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<string>): Observable<RemoteData<PaginatedList<Item>>> => {
const requestId = requestService.generateRequestId();
source.pipe(take(1)).subscribe((href: string) => {
const request = new GetRequest(requestId, href);
requestService.configure(request);
});
return rdb.buildList(source);
};

View File

@@ -38,11 +38,11 @@ class TestModel implements HALResource {
@Injectable()
class TestDataService {
findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
return 'findAllByHref';
}
findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<any>[]) {
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();
});
});

View File

@@ -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}`);

View File

@@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => {
let paginatedLinksToFollow;
beforeEach(() => {
paginatedLinksToFollow = [
followLink('page', undefined, true, ...linksToFollow),
followLink('page', undefined, true, true, true, ...linksToFollow),
...linksToFollow
];
});

View File

@@ -271,7 +271,7 @@ export class RemoteDataBuildService {
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, ...linksToFollow));
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, true, true, ...linksToFollow));
}
/**

View File

@@ -94,14 +94,16 @@ export class ServerSyncBufferEffects {
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
*/
private applyPatch(href: string): Observable<Action> {
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);

View File

@@ -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);
});
});
});

View File

@@ -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<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow).pipe(
public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
return this.dataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<ConfigObject>) => {
if (rd.hasFailed) {

View File

@@ -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<SubmissionFormsModel>[]): Observable<RemoteData<SubmissionFormsModel>> {
return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
public findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SubmissionFormsModel>[]): Observable<RemoteData<SubmissionFormsModel>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionFormsModel>>;
}
}

View File

@@ -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<RemoteData<SubmissionUploadsModel>> {
return super.findByHref(href, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionUploadsModel>>;
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionUploadsModel>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionUploadsModel>>;
}
}

View File

@@ -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({

View File

@@ -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));
});
});
});

View File

@@ -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<Bitstream> {
/**
* 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<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.findAllByHref(bundle._links.bitstreams.href, options, reRequestOnStale, ...linksToFollow);
findAllByBundle(bundle: Bundle, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
@@ -120,7 +122,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
switchMap((bundleRD: RemoteData<Bundle>) => {
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<PaginatedList<Bitstream>>) => {
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<Bitstream> {
* 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<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.bundleService.findByItemAndName(item, bundleName).pipe(
switchMap((bundleRD: RemoteData<Bundle>) => {
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<Bitstream> {
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');

View File

@@ -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',

View File

@@ -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<BitstreamFormat> {
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<BitstreamFormat> {
map((endpointURL: string) => {
return new PostRequest(requestId, endpointURL, bitstreamFormat);
}),
configureRequest(this.requestService)
sendRequest(this.requestService)
).subscribe();
return this.rdbService.buildFromRequestUUID(requestId);

View File

@@ -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);
});
});

View File

@@ -50,28 +50,34 @@ export class BundleDataService extends DataService<Bundle> {
/**
* 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<Bundle>[]): Observable<RemoteData<PaginatedList<Bundle>>> {
return this.findAllByHref(item._links.bundles.href, options, reRequestOnStale, ...linksToFollow);
findAllByItem(item: Item, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<PaginatedList<Bundle>>> {
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<Bundle>[]): Observable<RemoteData<Bundle>> {
return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, reRequestOnStale, ...linksToFollow).pipe(
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
map((rd: RemoteData<PaginatedList<Bundle>>) => {
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<Bundle> {
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<Bitstream>(hrefObs, ...linksToFollow);

View File

@@ -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(() => {

View File

@@ -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<Collection> {
/**
* 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<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollection(query: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
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<PaginatedList<Collection>>) => !collections.isResponsePending));
}
@@ -149,7 +146,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
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<ContentSource>(href$);
@@ -175,9 +172,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
);
// 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<ContentSource>(requestId).pipe(
@@ -205,48 +200,6 @@ export class CollectionDataService extends ComColDataService<Collection> {
);
}
/**
* Fetches the endpoint used for mapping items to a collection
* @param collectionId The id of the collection to map items to
*/
getMappedItemsEndpoint(collectionId): Observable<string> {
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<Item>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
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<ResponseParsingService> {
return DSOResponseParsingService;
}
});
}),
configureRequest(this.requestService)
).subscribe();
return this.rdbService.buildList(href$, ...linksToFollow);
}
protected getFindByParentHref(parentUUID: string): Observable<string> {
return this.halService.getEndpoint('communities').pipe(
switchMap((communityEndpointHref: string) =>
@@ -261,5 +214,4 @@ export class CollectionDataService extends ComColDataService<Collection> {
findOwningCollectionFor(item: Item): Observable<RemoteData<Collection>> {
return this.findByHref(item._links.owningCollection.href);
}
}

View File

@@ -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);

View File

@@ -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<T extends Community | Collection> 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<Community>(href)),
@@ -71,7 +70,7 @@ export abstract class ComColDataService<T extends Community | Collection> extend
const href$ = this.getFindByParentHref(parentUUID).pipe(
map((href: string) => this.buildHrefFromFindOptions(href, options))
);
return this.findList(href$, options);
return this.findAllByHref(href$);
}
/**

View File

@@ -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<Community> {
findTop(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
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<Community>(hrefObs) as Observable<RemoteData<PaginatedList<Community>>>;
return this.findAllByHref(hrefObs, undefined);
}
protected getFindByParentHref(parentUUID: string): Observable<string> {

View File

@@ -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<ConfigurationProperty> for the object with the given name', () => {

View File

@@ -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));
});
});

View File

@@ -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<T extends CacheableObject> 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<T extends CacheableObject> 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<T extends CacheableObject> 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<T>[]) {
protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]) {
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
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<T extends CacheableObject> 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<RemoteData<PaginatedList<T>>>}
* Return an observable that emits object list
*/
findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
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<T>[]) {
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<T>(href$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.findList(href$, options, reRequestOnStale, ...linksToFollow))
) as Observable<RemoteData<PaginatedList<T>>>;
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
@@ -312,79 +301,108 @@ export abstract class DataService<T extends CacheableObject> 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<T>[]): Observable<RemoteData<T>> {
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<T>(href$, ...linksToFollow).pipe(
reRequestStaleRemoteData(reRequestOnStale, () =>
this.findById(id, reRequestOnStale, ...linksToFollow))
);
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
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<string>
* @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<T>[]): Observable<RemoteData<T>> {
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<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
this.requestService.configure(request);
return this.rdbService.buildSingle<T>(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<T>(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<string>
* @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<T>[]): Observable<RemoteData<PaginatedList<T>>> {
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<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
this.requestService.configure(request);
return this.rdbService.buildList<T>(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<T>(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<string>
* @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<string>, 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<T extends CacheableObject> 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<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
searchBy(searchMethod: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
const requestId = this.requestService.generateRequestId();
searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
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<T extends CacheableObject> 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<Operation[]> {
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<T extends CacheableObject> 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<T extends CacheableObject> 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<T extends CacheableObject> implements UpdateDa
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
this.requestService.send(request);
});
const result$ = this.rdbService.buildFromRequestUUID<T>(requestId);
@@ -579,7 +586,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
this.requestService.send(request);
return this.rdbService.buildFromRequestUUID(requestId);
}

View File

@@ -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);
});
});

View File

@@ -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<any> {
@@ -53,7 +54,7 @@ export class DsoRedirectDataService extends DataService<any> {
{}, [], ...linksToFollow);
}
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<DSpaceObject>> {
this.setLinkPath(identifierType);
return this.findById(id).pipe(
getFirstCompletedRemoteData(),

View File

@@ -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<DSpaceObject> for the object with the given ID', () => {

View File

@@ -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<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findById(id, reRequestOnStale, ...linksToFollow);
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
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<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow);
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<DSpaceObject>> {
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<DSpaceObject>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -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<ItemType> {
protected linkPath = 'entitytypes';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
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<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
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<ItemType>[]): Observable<RemoteData<ItemType>> {
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<ItemType>[]): Observable<RemoteData<PaginatedList<ItemType>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -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<ItemType> {
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected relationshipTypeService: RelationshipTypeService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
super();
@@ -69,18 +70,16 @@ export class EntityTypeService extends DataService<ItemType> {
/**
* 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<RelationshipType>[]): Observable<RemoteData<PaginatedList<RelationshipType>>> {
getEntityTypeRelationships(entityTypeId: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<RelationshipType>[]): Observable<RemoteData<PaginatedList<RelationshipType>>> {
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);
}
/**

View File

@@ -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 }));
});
});

View File

@@ -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();

View File

@@ -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', () => {

View File

@@ -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<ExternalSource> {
/**
* 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<RemoteData<PaginatedList<ExternalSourceEntry>>> {
const requestUuid = this.requestService.generateRequestId();
getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ExternalSourceEntry>[]): Observable<RemoteData<PaginatedList<ExternalSourceEntry>>> {
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;
}
}

View File

@@ -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);
});
});
});

View File

@@ -60,7 +60,7 @@ export class AuthorizationDataService extends DataService<Authorization> {
* @param featureId ID of the {@link Feature} to check {@link Authorization} for
*/
isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable<boolean> {
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<Authorization> {
/**
* 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<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Authorization>[]): Observable<RemoteData<PaginatedList<Authorization>>> {
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);
})
);
}

View File

@@ -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<any>[];
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);
});
});
});
});

View File

@@ -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<any> {
// linkPath isn't used if we're only searching by href.
protected linkPath = undefined;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<any>) {
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<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<any>) {
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<T extends CacheableObject>(href: string | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
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<T extends CacheableObject>(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -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();
});
});

View File

@@ -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<Item> {
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<Item> {
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<RemoteData<PaginatedList<Collection>>> {
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<Item> {
take(1)
).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
this.requestService.send(request);
});
return this.rdbService.buildList<Bundle>(hrefObs);
@@ -225,7 +207,7 @@ export class ItemDataService extends DataService<Item> {
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<Item> {
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<Item> {
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();

View File

@@ -28,7 +28,7 @@ describe('ItemTemplateDataService', () => {
generateRequestId(): string {
return scopeID;
},
configure(request: RestRequest) {
send(request: RestRequest) {
// Do nothing
},
getByHref(requestHref: string) {

View File

@@ -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<Item>[]): Observable<RemoteData<Item>> {
findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
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<Item> {
/**
* 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<Item>[]): Observable<RemoteData<Item>> {
return this.dataService.findByCollectionID(collectionID, reRequestOnStale, ...linksToFollow);
findByCollectionID(collectionID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
return this.dataService.findByCollectionID(collectionID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**

View File

@@ -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<License> {
protected linkPath = '';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<License>) {
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<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<License>) {
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<License>[]): Observable<RemoteData<License>> {
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<License>[]): Observable<RemoteData<PaginatedList<License>>> {
return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -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);
});
});
});

View File

@@ -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<MetadataField> {
/**
* 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<MetadataField>[]) {
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]) {
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<MetadataField> {
* 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<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> {
searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> {
const optionParams = Object.assign(new FindListOptions(), options, {
searchParams: [
new RequestParam('schema', hasValue(schema) ? schema : ''),
@@ -84,7 +88,7 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
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<MetadataField> {
* Clear all metadata field requests
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
*/
clearRequests(): Observable<string> {
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);
});
}
}

View File

@@ -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();
});
});

View File

@@ -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<Process> {
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected bitstreamDataService: BitstreamDataService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Process>) {
super();
@@ -48,20 +48,11 @@ export class ProcessDataService extends DataService<Process> {
}
/**
* Get a process his output files
* Get a process' output files
* @param processId The ID of the process
*/
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
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$);
}
}

View File

@@ -49,7 +49,7 @@ export class ScriptDataService extends DataService<Script> {
const body = this.getInvocationFormData(parameters, files);
return new MultipartPostRequest(requestId, endpoint, body);
})
).subscribe((request: RestRequest) => this.requestService.configure(request));
).subscribe((request: RestRequest) => this.requestService.send(request));
return this.rdbService.buildFromRequestUUID<Process>(requestId);
}

Some files were not shown because too many files have changed in this diff Show More