diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c418704b3..0495f51fc2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,6 +65,13 @@ jobs: - name: Run specs (unit tests) run: yarn run test:headless + # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 + # Upload coverage reports to Codecov (for Node v12 only) + # https://github.com/codecov/codecov-action + - name: Upload coverage to Codecov.io + uses: codecov/codecov-action@v1 + if: matrix.node-version == '12.x' + # Using docker-compose start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) @@ -78,10 +85,3 @@ jobs: - name: Shutdown Docker containers run: docker-compose -f ./docker/docker-compose-ci.yml down - - # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - # Upload coverage reports to Codecov (for Node v12 only) - # https://github.com/codecov/codecov-action - - name: Upload coverage to Codecov.io - uses: codecov/codecov-action@v1 - if: matrix.node-version == '12.x' diff --git a/angular.json b/angular.json index d8d8a2dc2e..fcb2d968f0 100644 --- a/angular.json +++ b/angular.json @@ -94,6 +94,12 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", + "sourceMap": { + "scripts": false, + "styles": false, + "hidden": false, + "vendor": false + }, "assets": [ "src/assets" ], diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts index 3cc44118cf..60f9933fb3 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts @@ -8,7 +8,7 @@ import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { FindListOptions } from '../../../core/data/request.models'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -44,7 +44,7 @@ describe('EPeopleRegistryComponent', () => { activeEPerson: null, allEpeople: mockEPeople, getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); @@ -54,18 +54,18 @@ describe('EPeopleRegistryComponent', () => { const result = this.allEpeople.find((ePerson: EPerson) => { return ePerson.email === query }); - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); } if (scope === 'metadata') { if (query === '') { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); } const result = this.allEpeople.find((ePerson: EPerson) => { return (ePerson.name.includes(query) || ePerson.email.includes(query)) }); - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople)); }, deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts index fe0503bcd7..8122e8483d 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts @@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { Subscription } from 'rxjs/internal/Subscription'; import { map, switchMap, take } from 'rxjs/operators'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPerson } from '../../../core/eperson/models/eperson.model'; @@ -15,13 +15,16 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio 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 } from '../../../core/shared/operators'; -import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RequestService } from '../../../core/data/request.service'; import { filter } from 'rxjs/internal/operators/filter'; import { PageInfo } from '../../../core/shared/page-info.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ selector: 'ds-epeople-registry', @@ -159,7 +162,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { }) ); })).pipe(map((dtos: EpersonDtoModel[]) => { - return new PaginatedList(epeople.pageInfo, dtos); + return buildPaginatedList(epeople.pageInfo, dtos); })) })).subscribe((value) => { this.ePeopleDto$.next(value); @@ -215,13 +218,12 @@ export class EPeopleRegistryComponent 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: RestResponse) => { - if (restResponse.isSuccessful) { + this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { + if (restResponse.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name })); this.reset(); } else { - const errorResponse = restResponse as ErrorResponse; - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + errorResponse.statusCode + ' and message: ' + errorResponse.errorMessage); + this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); } }) }} diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 1119107a85..36b9360899 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -1,14 +1,13 @@ import { of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; -import { RestResponse } from '../../../../core/cache/response.models'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { FindListOptions } from '../../../../core/data/request.models'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; @@ -46,7 +45,7 @@ describe('EPersonFormComponent', () => { activeEPerson: null, allEpeople: mockEPeople, getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); @@ -56,18 +55,18 @@ describe('EPersonFormComponent', () => { const result = this.allEpeople.find((ePerson: EPerson) => { return ePerson.email === query }); - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [result])); } if (scope === 'metadata') { if (query === '') { - return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); } const result = this.allEpeople.find((ePerson: EPerson) => { return (ePerson.name.includes(query) || ePerson.email.includes(query)) }); - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [result])); } - return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); }, deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { @@ -75,8 +74,9 @@ describe('EPersonFormComponent', () => { }); return observableOf(true); }, - create(ePerson: EPerson) { - this.allEpeople = [...this.allEpeople, ePerson] + create(ePerson: EPerson): Observable> { + this.allEpeople = [...this.allEpeople, ePerson]; + return createSuccessfulRemoteDataObject$(ePerson); }, editEPerson(ePerson: EPerson) { this.activeEPerson = ePerson; @@ -87,18 +87,13 @@ describe('EPersonFormComponent', () => { clearEPersonRequests(): void { // empty }, - tryToCreate(ePerson: EPerson): Observable { - this.allEpeople = [...this.allEpeople, ePerson] - return observableOf(new RestResponse(true, 200, 'Success')); - }, - updateEPerson(ePerson: EPerson): Observable { + updateEPerson(ePerson: EPerson): Observable> { this.allEpeople.forEach((ePersonInList: EPerson, i: number) => { if (ePersonInList.id === ePerson.id) { this.allEpeople[i] = ePerson; } }); - return observableOf(new RestResponse(true, 200, 'Success')); - + return createSuccessfulRemoteDataObject$(ePerson); } }; builderService = getMockFormBuilderService(); @@ -299,7 +294,7 @@ describe('EPersonFormComponent', () => { it ('should call the epersonFormComponent delete when clicked on the button' , () => { spyOn(component, 'delete').and.stub(); - spyOn(component.epersonService, 'deleteEPerson').and.returnValue(observableOf(new RestResponse(true, 204, 'No Content'))); + spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204)); const deleteButton = fixture.debugElement.query(By.css('.delete-button')); deleteButton.triggerEventHandler('click', null); expect(component.delete).toHaveBeenCalled(); @@ -307,7 +302,7 @@ describe('EPersonFormComponent', () => { it ('should call the epersonService delete when clicked on the button' , () => { // ePersonDataServiceStub.activeEPerson = eperson; - spyOn(component.epersonService, 'deleteEPerson').and.returnValue(observableOf(new RestResponse(true, 204, 'No Content'))); + spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204)); const deleteButton = fixture.debugElement.query(By.css('.delete-button')); expect(deleteButton.nativeElement.disabled).toBe(false); deleteButton.triggerEventHandler('click', null); diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index 61087034b6..6f0ad23f78 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -10,14 +10,17 @@ import { TranslateService } from '@ngx-translate/core'; import { Subscription, combineLatest, of } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { switchMap, take } from 'rxjs/operators'; -import { RestResponse } from '../../../../core/cache/response.models'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { Group } from '../../../../core/eperson/models/group.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { + getRemoteDataPayload, + getFirstSucceededRemoteData, + getFirstCompletedRemoteData +} from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; @@ -28,6 +31,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RequestService } from '../../../../core/data/request.service'; +import { NoContent } from '../../../../core/shared/NoContent.model'; @Component({ selector: 'ds-eperson-form', @@ -314,9 +318,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { createNewEPerson(values) { const ePersonToCreate = Object.assign(new EPerson(), values); - const response = this.epersonService.tryToCreate(ePersonToCreate); - response.pipe(take(1)).subscribe((restResponse: RestResponse) => { - if (restResponse.isSuccessful) { + const response = this.epersonService.create(ePersonToCreate); + response.pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name })); this.submitForm.emit(ePersonToCreate); } else { @@ -354,8 +360,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy { }); const response = this.epersonService.updateEPerson(editedEperson); - response.pipe(take(1)).subscribe((restResponse: RestResponse) => { - if (restResponse.isSuccessful) { + response.pipe(take(1)).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name })); this.submitForm.emit(editedEperson); } else { @@ -380,7 +386,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.subs.push(this.epersonService.searchByScope('email', ePerson.email, { currentPage: 1, elementsPerPage: 0 - }).pipe(getSucceededRemoteData(), getRemoteDataPayload()) + }).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', { @@ -434,12 +440,12 @@ 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: RestResponse) => { - if (restResponse.isSuccessful) { + this.epersonService.deleteEPerson(eperson).pipe(take(1)).subscribe((restResponse: RemoteData) => { + if (restResponse.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name })); this.reset(); } else { - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.statusText); + this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); } this.cancelForm.emit(); }) diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts index 07b5a00ea3..e07e9ade60 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts @@ -12,11 +12,10 @@ import { of as observableOf } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; -import { RestResponse } from '../../../../core/cache/response.models'; import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; @@ -35,6 +34,7 @@ import { getMockTranslateService } from '../../../../shared/mocks/translate.serv import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { Operation } from 'fast-json-patch'; describe('GroupFormComponent', () => { let component: GroupFormComponent; @@ -71,6 +71,7 @@ describe('GroupFormComponent', () => { groupsDataServiceStub = { allGroups: groups, activeGroup: null, + createdGroup: null, getActiveGroup(): Observable { return observableOf(this.activeGroup); }, @@ -80,7 +81,10 @@ describe('GroupFormComponent', () => { editGroup(group: Group) { this.activeGroup = group }, - updateGroup(group: Group) { + clearGroupsRequests() { + return null; + }, + patch(group: Group, operations: Operation[]) { return null; }, cancelEditGroup(): void { @@ -89,12 +93,21 @@ describe('GroupFormComponent', () => { findById(id: string) { return observableOf({ payload: null, hasSucceeded: true }); }, - tryToCreate(group: Group): Observable { - this.allGroups = [...this.allGroups, group] - return observableOf(new RestResponse(true, 200, 'Success')); + findByHref(href: string) { + return createSuccessfulRemoteDataObject$(this.createdGroup); + }, + create(group: Group): Observable> { + this.allGroups = [...this.allGroups, group]; + this.createdGroup = Object.assign({}, group, { + _links: { self: { href: 'group-selflink' } } + }); + return createSuccessfulRemoteDataObject$(this.createdGroup); }, searchGroups(query: string): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])) + }, + getGroupEditPageRouterLinkWithID(id: string) { + return `group-edit-page-for-${id}`; } }; authorizationService = jasmine.createSpyObj('authorizationService', { @@ -171,7 +184,7 @@ describe('GroupFormComponent', () => { describe('with active Group', () => { beforeEach(() => { spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); - spyOn(groupsDataServiceStub, 'updateGroup').and.returnValue(observableOf(new RestResponse(true, 200, 'OK'))); + spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected)); component.groupName.value = 'newGroupName'; component.onSubmit(); fixture.detectChanges(); diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index 2306c675c8..104aec46ae 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -9,15 +9,20 @@ import { DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; -import { ObservedValueOf, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { + ObservedValueOf, + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + Subscription +} from 'rxjs'; import { catchError, map, switchMap, take } from 'rxjs/operators'; import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths'; -import { RestResponse } from '../../../../core/cache/response.models'; import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { RequestService } from '../../../../core/data/request.service'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; @@ -26,13 +31,19 @@ import { Group } from '../../../../core/eperson/models/group.model'; import { Collection } from '../../../../core/shared/collection.model'; import { Community } from '../../../../core/shared/community.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { + getRemoteDataPayload, + getFirstSucceededRemoteData, + getFirstCompletedRemoteData +} from '../../../../core/shared/operators'; import { AlertType } from '../../../../shared/alert/aletr-type'; import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; +import { hasValue, isNotEmpty, hasValueOperator } from '../../../../shared/empty.util'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { NoContent } from '../../../../core/shared/NoContent.model'; +import { Operation } from 'fast-json-patch'; @Component({ selector: 'ds-group-form', @@ -135,6 +146,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.setActiveGroup(params.groupId) })); this.canEdit$ = this.groupDataService.getActiveGroup().pipe( + hasValueOperator(), switchMap((group: Group) => { return observableCombineLatest( this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), @@ -231,17 +243,17 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ createNewGroup(values) { const groupToCreate = Object.assign(new Group(), values); - const response = this.groupDataService.tryToCreate(groupToCreate); - response.pipe(take(1)).subscribe((restResponse: RestResponse) => { - if (restResponse.isSuccessful) { + this.groupDataService.create(groupToCreate).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name })); this.submitForm.emit(groupToCreate); - const resp: any = restResponse; - if (isNotEmpty(resp.resourceSelfLinks)) { - const groupSelfLink = resp.resourceSelfLinks[0]; + if (isNotEmpty(rd.payload)) { + const groupSelfLink = rd.payload._links.self.href; this.setActiveGroupWithLink(groupSelfLink); this.groupDataService.clearGroupsRequests(); - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(this.groupDataService.getUUIDFromString(groupSelfLink))); + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid)); } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name })); @@ -262,7 +274,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.subs.push(this.groupDataService.searchGroups(group.name, { currentPage: 1, elementsPerPage: 0 - }).pipe(getSucceededRemoteData(), getRemoteDataPayload()) + }).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', { @@ -277,25 +289,32 @@ export class GroupFormComponent implements OnInit, OnDestroy { * @param group Group to edit and old values contained within */ editGroup(group: Group) { - const editedGroup = Object.assign(new Group(), { - id: group.id, - metadata: { - 'dc.description': [ - { - value: (hasValue(this.groupDescription.value) ? this.groupDescription.value : group.firstMetadataValue('dc.description')) - } - ], - }, - name: (hasValue(this.groupName.value) ? this.groupName.value : group.name), - _links: group._links, - }); - const response = this.groupDataService.updateGroup(editedGroup); - response.pipe(take(1)).subscribe((restResponse: RestResponse) => { - if (restResponse.isSuccessful) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: editedGroup.name })); - this.submitForm.emit(editedGroup); + let operations: Operation[] = [] + + if (hasValue(this.groupDescription.value)) { + operations = [...operations, { + op: 'replace', + path: '/metadata/dc.description/0/value', + value: this.groupDescription.value + }]; + } + + if (hasValue(this.groupName.value)) { + operations = [...operations, { + op: 'replace', + path: '/name', + value: this.groupName.value + }]; + } + + this.groupDataService.patch(group, operations).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: rd.payload.name })); + this.submitForm.emit(rd.payload); } else { - this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: editedGroup.name })); + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: group.name })); this.cancelForm.emit(); } }); @@ -309,7 +328,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupDataService.cancelEditGroup(); this.groupDataService.findById(groupId) .pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((group: Group) => { this.groupDataService.editGroup(group); @@ -324,9 +343,9 @@ 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, followLink('subgroups'), followLink('epersons'), followLink('object')) + this.groupDataService.findByHref(groupSelfLink, false, followLink('subgroups'), followLink('epersons'), followLink('object')) .pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((group: Group) => { this.groupDataService.editGroup(group); @@ -350,15 +369,15 @@ export class GroupFormComponent implements OnInit, OnDestroy { modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => { if (confirm) { if (hasValue(group.id)) { - this.groupDataService.deleteGroup(group).pipe(take(1)) - .subscribe(([success, optionalErrorMessage]: [boolean, string]) => { - if (success) { + this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData()) + .subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name })); this.reset(); } else { this.notificationsService.error( this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }), - this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: optionalErrorMessage })); + this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage })); } }) } diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html index ccbb64d42c..d24bf35e2a 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -24,7 +24,7 @@ - - - -

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

- - diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index 68942289ff..1add6e3aa3 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -8,7 +8,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; import { RestResponse } from '../../../../../core/cache/response.models'; -import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../../core/data/remote-data'; import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { Group } from '../../../../../core/eperson/models/group.model'; @@ -52,16 +52,16 @@ describe('SubgroupsListComponent', () => { return this.activeGroup; }, findAllByHref(href: string): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.subgroups)) + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.subgroups)) }, getGroupEditPageRouterLink(group: Group): string { return '/admin/access-control/groups/' + group.id; }, searchGroups(query: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allGroups)) + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allGroups)) } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])) }, addSubGroupToGroup(parentGroup, subgroup: Group): Observable { this.subgroups = [...this.subgroups, subgroup]; diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index 62927b74aa..96177c5a11 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -4,12 +4,15 @@ import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf, Subscription } from 'rxjs'; import { map, mergeMap, take } from 'rxjs/operators'; -import { RestResponse } from '../../../../../core/cache/response.models'; -import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../../core/data/remote-data'; import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { Group } from '../../../../../core/eperson/models/group.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { + getRemoteDataPayload, + 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'; @@ -125,7 +128,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { elementsPerPage: Number.MAX_SAFE_INTEGER }) .pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)), map((groups: Group[]) => groups.length > 0)) @@ -231,9 +234,9 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { * @param nameObject Object request was about * @param activeGroup Group currently being edited */ - showNotifications(messageSuffix: string, response: Observable, nameObject: string, activeGroup: Group) { - response.pipe(take(1)).subscribe((restResponse: RestResponse) => { - if (restResponse.isSuccessful) { + showNotifications(messageSuffix: string, response: Observable>, nameObject: string, activeGroup: Group) { + response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts index 1d091e75d1..4de9d9e125 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts @@ -9,7 +9,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -47,11 +47,11 @@ describe('GroupRegistryComponent', () => { findAllByHref(href: string): Observable>> { switch (href) { case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons': - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons': - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [EPersonMock])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [EPersonMock])); default: - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); } } }; @@ -60,11 +60,11 @@ describe('GroupRegistryComponent', () => { findAllByHref(href: string): Observable>> { switch (href) { case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups': - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups': - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [GroupMock2])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [GroupMock2])); default: - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), [])); } }, getGroupEditPageRouterLink(group: Group): string { @@ -75,12 +75,12 @@ describe('GroupRegistryComponent', () => { }, searchGroups(query: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allGroups.length, totalElements: this.allGroups.length, totalPages: 1, currentPage: 1 }), this.allGroups)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allGroups.length, totalElements: this.allGroups.length, totalPages: 1, currentPage: 1 }), this.allGroups)); } const result = this.allGroups.find((group: Group) => { return (group.id.includes(query)) }); - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result])); } }; dsoDataServiceStub = { diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index d4a1374cfb..30a5842a71 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -2,14 +2,20 @@ 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, of as observableOf } from 'rxjs'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, + Observable, + of as observableOf +} from 'rxjs'; import { filter } from 'rxjs/internal/operators/filter'; import { ObservedValueOf } from 'rxjs/internal/types'; 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'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -19,11 +25,15 @@ import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model'; import { Group } from '../../../core/eperson/models/group.model'; import { RouteService } from '../../../core/services/route.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { PageInfo } from '../../../core/shared/page-info.model'; 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'; @Component({ selector: 'ds-groups-registry', @@ -115,7 +125,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.subs.push(this.groupService.searchGroups(this.currentSearchQuery.trim(), { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize - }).subscribe((groupsRD: RemoteData>) => { + }).pipe(getFirstCompletedRemoteData()) + .subscribe((groupsRD: RemoteData>) => { this.groups$.next(groupsRD); this.pageInfoState$.next(groupsRD.payload.pageInfo); } @@ -136,7 +147,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { } ) })).pipe(map((dtos: GroupDtoModel[]) => { - return new PaginatedList(groups.pageInfo, dtos); + return buildPaginatedList(groups.pageInfo, dtos); })) })).subscribe((value: PaginatedList) => { this.groupsDto$.next(value); @@ -149,15 +160,15 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { */ deleteGroup(group: Group) { if (hasValue(group.id)) { - this.groupService.deleteGroup(group).pipe(take(1)) - .subscribe(([success, optionalErrorMessage]: [boolean, string]) => { - if (success) { + this.groupService.delete(group.id).pipe(getFirstCompletedRemoteData()) + .subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name })); this.reset(); } else { this.notificationsService.error( this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }), - this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: optionalErrorMessage })); + this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage })); } }) } diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts index 9c4efb6796..e6e242755f 100644 --- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts @@ -19,6 +19,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive'; import { FileValidator } from '../../shared/utils/require-file.validator'; import { MetadataImportPageComponent } from './metadata-import-page.component'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; describe('MetadataImportPageComponent', () => { let comp: MetadataImportPageComponent; @@ -36,13 +37,7 @@ describe('MetadataImportPageComponent', () => { notificationService = new NotificationsServiceStub(); scriptService = jasmine.createSpyObj('scriptService', { - invoke: observableOf({ - response: - { - isSuccessful: true, - resourceSelfLinks: ['https://localhost:8080/api/core/processes/45'] - } - }) + invoke: createSuccessfulRemoteDataObject$({ processId: '45' }) } ); user = Object.assign(new EPerson(), { @@ -133,12 +128,7 @@ describe('MetadataImportPageComponent', () => { describe('if proceed is pressed; but script invoke fails', () => { beforeEach(fakeAsync(() => { jasmine.getEnv().allowRespy(true); - spyOn(scriptService, 'invoke').and.returnValue(observableOf({ - response: - { - isSuccessful: false, - } - })); + spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500)); const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; proceed.click(); fixture.detectChanges(); diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts index 3db6ad1c7c..25b060ce81 100644 --- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -3,14 +3,20 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; -import { map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; -import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service'; -import { RequestEntry } from '../../core/data/request.reducer'; +import { + METADATA_IMPORT_SCRIPT_NAME, + ScriptDataService +} from '../../core/data/processes/script-data.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { Process } from '../../process-page/processes/process.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; @Component({ selector: 'ds-metadata-import-page', @@ -79,28 +85,23 @@ export class MetadataImportPageComponent implements OnInit { Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), ]; return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]) - .pipe( - take(1), - map((requestEntry: RequestEntry) => { - if (requestEntry.response.isSuccessful) { - const title = this.translate.get('process.new.notification.success.title'); - const content = this.translate.get('process.new.notification.success.content'); - this.notificationsService.success(title, content); - const response: any = requestEntry.response; - if (isNotEmpty(response.resourceSelfLinks)) { - const processNumber = response.resourceSelfLinks[0].split('/').pop(); - this.router.navigateByUrl('/processes/' + processNumber); - } - } else { - const title = this.translate.get('process.new.notification.error.title'); - const content = this.translate.get('process.new.notification.error.content'); - this.notificationsService.error(title, content); - } - })); } }), - take(1) - ).subscribe(); + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + const title = this.translate.get('process.new.notification.success.title'); + const content = this.translate.get('process.new.notification.success.content'); + this.notificationsService.success(title, content); + if (isNotEmpty(rd.payload)) { + this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); + } + } else { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.content'); + this.notificationsService.error(title, content); + } + }); } } } diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts index 8b33463aa0..a8b510cca6 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts @@ -6,7 +6,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../../core/cache/response.models'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; @@ -14,6 +13,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../../../shared/testing/router.stub'; import { AddBitstreamFormatComponent } from './add-bitstream-format.component'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; describe('AddBitstreamFormatComponent', () => { let comp: AddBitstreamFormatComponent; @@ -37,7 +37,7 @@ describe('AddBitstreamFormatComponent', () => { router = new RouterStub(); notificationService = new NotificationsServiceStub(); bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { - createBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')), + createBitstreamFormat: createSuccessfulRemoteDataObject$({}), clearBitStreamFormatRequests: observableOf(null) }); @@ -77,7 +77,7 @@ describe('AddBitstreamFormatComponent', () => { router = new RouterStub(); notificationService = new NotificationsServiceStub(); bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { - createBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')), + createBitstreamFormat: createFailedRemoteDataObject$('Error', 500), clearBitStreamFormatRequests: observableOf(null) }); diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts index ecc26bd5d9..132343dcf0 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts @@ -1,12 +1,12 @@ -import { take } from 'rxjs/operators'; import { Router } from '@angular/router'; import { Component } from '@angular/core'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; -import { RestResponse } from '../../../../core/cache/response.models'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; /** * This component renders the page to create a new bitstream format. @@ -32,9 +32,10 @@ export class AddBitstreamFormatComponent { * @param bitstreamFormat */ createBitstreamFormat(bitstreamFormat: BitstreamFormat) { - this.bitstreamFormatDataService.createBitstreamFormat(bitstreamFormat).pipe(take(1) - ).subscribe((response: RestResponse) => { - if (response.isSuccessful) { + this.bitstreamFormatDataService.createBitstreamFormat(bitstreamFormat).pipe( + getFirstCompletedRemoteData(), + ).subscribe((response: RemoteData) => { + if (response.hasSucceeded) { this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.create.success.head'), this.translateService.get('admin.registries.bitstream-formats.create.success.content')); this.router.navigate([getBitstreamFormatsModuleRoute()]); diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 3037744e67..0e426a20e3 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -1,8 +1,6 @@ import { BitstreamFormatsComponent } from './bitstream-formats.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; @@ -19,6 +17,12 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; +import { + createNoContentRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; @@ -73,7 +77,7 @@ describe('BitstreamFormatsComponent', () => { bitstreamFormat3, bitstreamFormat4 ]; - const mockFormatsRD = new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList)); + const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList)); const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); @@ -82,12 +86,12 @@ describe('BitstreamFormatsComponent', () => { bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), - find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])), + find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), getSelectedBitstreamFormats: hot('a', {a: mockFormatsList}), selectBitstreamFormat: {}, deselectBitstreamFormat: {}, deselectAllBitstreamFormats: {}, - delete: observableOf(true), + delete: createSuccessfulRemoteDataObject$({}), clearBitStreamFormatRequests: observableOf('cleared') }); @@ -204,12 +208,12 @@ describe('BitstreamFormatsComponent', () => { bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), - find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])), + find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), getSelectedBitstreamFormats: observableOf(mockFormatsList), selectBitstreamFormat: {}, deselectBitstreamFormat: {}, deselectAllBitstreamFormats: {}, - delete: observableOf({ isSuccessful: true }), + delete: createNoContentRemoteDataObject$(), clearBitStreamFormatRequests: observableOf('cleared') }); @@ -250,7 +254,7 @@ describe('BitstreamFormatsComponent', () => { bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), - find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])), + find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), getSelectedBitstreamFormats: observableOf(mockFormatsList), selectBitstreamFormat: {}, deselectBitstreamFormat: {}, diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 80ae56ec93..869e57ef89 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; @@ -11,7 +11,7 @@ import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { RestResponse } from '../../../core/cache/response.models'; +import { NoContent } from '../../../core/shared/NoContent.model'; /** * This component renders a list of bitstream formats @@ -65,7 +65,7 @@ export class BitstreamFormatsComponent implements OnInit { const tasks$ = []; for (const format of formats) { if (hasValue(format.id)) { - tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RestResponse) => response.isSuccessful))); + tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RemoteData) => response.hasSucceeded))); } } zip(...tasks$).subscribe((results: boolean[]) => { diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts index f6eef741fd..a00f660baf 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; -import { hasValue } from '../../../shared/empty.util'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; /** * This class represents a resolver that requests a specific bitstreamFormat before the route is activated @@ -25,7 +24,7 @@ export class BitstreamFormatsResolver implements Resolve> { return this.bitstreamFormatDataService.findById(route.params.id) .pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + getFirstCompletedRemoteData() ); } } diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts index 08770dddf2..a8bd41d569 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts @@ -6,7 +6,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../../core/cache/response.models'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level'; @@ -15,6 +14,11 @@ import { NotificationsService } from '../../../../shared/notifications/notificat import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../../../shared/testing/router.stub'; import { EditBitstreamFormatComponent } from './edit-bitstream-format.component'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../../shared/remote-data.utils'; describe('EditBitstreamFormatComponent', () => { let comp: EditBitstreamFormatComponent; @@ -32,7 +36,7 @@ describe('EditBitstreamFormatComponent', () => { const routeStub = { data: observableOf({ - bitstreamFormat: new RemoteData(false, false, true, null, bitstreamFormat) + bitstreamFormat: createSuccessfulRemoteDataObject(bitstreamFormat) }) }; @@ -44,7 +48,7 @@ describe('EditBitstreamFormatComponent', () => { router = new RouterStub(); notificationService = new NotificationsServiceStub(); bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { - updateBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')) + updateBitstreamFormat: createSuccessfulRemoteDataObject$({}) }); TestBed.configureTestingModule({ @@ -73,7 +77,8 @@ describe('EditBitstreamFormatComponent', () => { it('should initialise the bitstreamFormat based on the route', () => { comp.bitstreamFormatRD$.subscribe((format: RemoteData) => { - expect(format).toEqual(new RemoteData(false, false, true, null, bitstreamFormat)); + const expected = createSuccessfulRemoteDataObject(bitstreamFormat); + expect(format.payload).toEqual(expected.payload); }); }); }); @@ -94,7 +99,7 @@ describe('EditBitstreamFormatComponent', () => { router = new RouterStub(); notificationService = new NotificationsServiceStub(); bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { - updateBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')) + updateBitstreamFormat: createFailedRemoteDataObject$('Error', 500) }); TestBed.configureTestingModule({ diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts index 0b63e4d4dd..14b109fe26 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts @@ -1,14 +1,14 @@ -import { map, take } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { Component, OnInit } from '@angular/core'; import { RemoteData } from '../../../../core/data/remote-data'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; -import { RestResponse } from '../../../../core/cache/response.models'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; /** * This component renders the edit page of a bitstream format. @@ -46,9 +46,10 @@ export class EditBitstreamFormatComponent implements OnInit { * When failed, an error notification will be shown. */ updateFormat(bitstreamFormat: BitstreamFormat) { - this.bitstreamFormatDataService.updateBitstreamFormat(bitstreamFormat).pipe(take(1) - ).subscribe((response: RestResponse) => { - if (response.isSuccessful) { + this.bitstreamFormatDataService.updateBitstreamFormat(bitstreamFormat).pipe( + getFirstCompletedRemoteData(), + ).subscribe((response: RemoteData) => { + if (response.hasSucceeded) { this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.edit.success.head'), this.translateService.get('admin.registries.bitstream-formats.edit.success.content')); this.router.navigate([getBitstreamFormatsModuleRoute()]); diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index bbfd256036..b0d48d5a10 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -1,7 +1,7 @@ import { MetadataRegistryComponent } from './metadata-registry.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; @@ -45,7 +45,7 @@ describe('MetadataRegistryComponent', () => { namespace: 'http://dspace.org/mockschema' } ]; - const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList)); + const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); /* tslint:disable:no-empty */ const registryServiceStub = { getMetadataSchemas: () => mockSchemas, diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts index cd845329c6..1e2e9551b4 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -2,18 +2,19 @@ import { Component } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; -import { RestResponse } from '../../../core/cache/response.models'; import { zip } from 'rxjs/internal/observable/zip'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Route, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-metadata-registry', @@ -140,12 +141,12 @@ export class MetadataRegistryComponent { const tasks$ = []; for (const schema of schemas) { if (hasValue(schema.id)) { - tasks$.push(this.registryService.deleteMetadataSchema(schema.id)); + tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); } } - zip(...tasks$).subscribe((responses: RestResponse[]) => { - const successResponses = responses.filter((response: RestResponse) => response.isSuccessful); - const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); + zip(...tasks$).subscribe((responses: Array>) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); if (successResponses.length > 0) { this.showNotification(true, successResponses.length); } diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index 9532a64e84..9f14d31aad 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -1,7 +1,7 @@ import { MetadataSchemaComponent } from './metadata-schema.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; @@ -100,11 +100,11 @@ describe('MetadataSchemaComponent', () => { schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]) } ]; - const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList)); + const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); /* tslint:disable:no-empty */ const registryServiceStub = { getMetadataSchemas: () => mockSchemas, - getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), + getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]), getActiveMetadataField: () => observableOf(undefined), getSelectedMetadataFields: () => observableOf([]), diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts index 87f4b863c0..6b927b9796 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -3,21 +3,24 @@ import { RegistryService } from '../../../core/registry/registry.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; -import { RestResponse } from '../../../core/cache/response.models'; import { zip } from 'rxjs/internal/observable/zip'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ selector: 'ds-metadata-schema', @@ -92,7 +95,7 @@ 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), followLink('schema')); + return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), true, followLink('schema')); } }) ); @@ -168,12 +171,12 @@ export class MetadataSchemaComponent implements OnInit { const tasks$ = []; for (const field of fields) { if (hasValue(field.id)) { - tasks$.push(this.registryService.deleteMetadataField(field.id)); + tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); } } - zip(...tasks$).subscribe((responses: RestResponse[]) => { - const successResponses = responses.filter((response: RestResponse) => response.isSuccessful); - const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); + zip(...tasks$).subscribe((responses: Array>) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); if (successResponses.length > 0) { this.showNotification(true, successResponses.length); } diff --git a/src/app/+bitstream-page/bitstream-page.resolver.ts b/src/app/+bitstream-page/bitstream-page.resolver.ts index 9c85688c39..a0520ad165 100644 --- a/src/app/+bitstream-page/bitstream-page.resolver.ts +++ b/src/app/+bitstream-page/bitstream-page.resolver.ts @@ -2,11 +2,10 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { RemoteData } from '../core/data/remote-data'; import { Observable } from 'rxjs/internal/Observable'; -import { find } from 'rxjs/operators'; -import { hasValue } from '../shared/empty.util'; import { Bitstream } from '../core/shared/bitstream.model'; import { BitstreamDataService } from '../core/data/bitstream-data.service'; import {followLink, FollowLinkConfig} from '../shared/utils/follow-link-config.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * This class represents a resolver that requests a specific bitstream before the route is activated @@ -24,9 +23,9 @@ export class BitstreamPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamService.findById(route.params.id, ...this.followLinks) + return this.bitstreamService.findById(route.params.id, false, ...this.followLinks) .pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + getFirstCompletedRemoteData(), ); } /** diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index ce46c2a7b3..fd10674961 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -2,7 +2,6 @@ import { EditBitstreamPageComponent } from './edit-bitstream-page.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; -import { RemoteData } from '../../core/data/remote-data'; import { of as observableOf } from 'rxjs/internal/observable/of'; import {ActivatedRoute, Router} from '@angular/router'; import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; @@ -17,16 +16,15 @@ import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; import { hasValue } from '../../shared/empty.util'; import { FormControl, FormGroup } from '@angular/forms'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; import { FileSizePipe } from '../../shared/utils/file-size-pipe'; -import { RestResponse } from '../../core/cache/response.models'; import { VarDirective } from '../../shared/utils/var.directive'; import { + createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {RouterStub} from '../../shared/testing/router.stub'; import { getItemEditRoute } from '../../+item-page/item-page-routing-paths'; +import { createPaginatedList } from '../../shared/testing/utils.test'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -109,7 +107,7 @@ describe('EditBitstreamPageComponent', () => { } ] }, - format: observableOf(new RemoteData(false, false, true, null, selectedFormat)), + format: createSuccessfulRemoteDataObject$(selectedFormat), _links: { self: 'bitstream-selflink' }, @@ -120,14 +118,14 @@ describe('EditBitstreamPageComponent', () => { }) }); bitstreamService = jasmine.createSpyObj('bitstreamService', { - findById: observableOf(new RemoteData(false, false, true, null, bitstream)), - update: observableOf(new RemoteData(false, false, true, null, bitstream)), - updateFormat: observableOf(new RestResponse(true, 200, 'OK')), + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, patch: {} }); bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { - findAll: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), allFormats))) + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats)) }); const itemPageUrl = `fake-url/some-uuid`; @@ -140,7 +138,7 @@ describe('EditBitstreamPageComponent', () => { providers: [ { provide: NotificationsService, useValue: notificationsService }, { provide: DynamicFormService, useValue: formService }, - { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: new RemoteData(false, false, true, null, bitstream) }), snapshot: { queryParams: {} } } }, + { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } }, { provide: BitstreamDataService, useValue: bitstreamService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, { provide: Router, useValue: routerStub }, diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index ad64739dac..fbb5cc7b8e 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, mergeMap, switchMap} from 'rxjs/operators'; +import { map, mergeMap, switchMap } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { Subscription } from 'rxjs/internal/Subscription'; import { @@ -22,22 +22,22 @@ import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, - getSucceededRemoteData + getFirstSucceededRemoteData, + getFirstCompletedRemoteData } from '../../core/shared/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; -import { RestResponse } from '../../core/cache/response.models'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { getItemEditRoute } from '../../+item-page/item-page-routing-paths'; -import {Bundle} from '../../core/shared/bundle.model'; -import {Item} from '../../core/shared/item.model'; +import { Bundle } from '../../core/shared/bundle.model'; +import { Item } from '../../core/shared/item.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -299,12 +299,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); const bitstream$ = this.bitstreamRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload() ); const allFormats$ = this.bitstreamFormatsRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload() ); @@ -438,16 +438,15 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { if (isNewFormat) { bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( - switchMap((formatResponse: RestResponse) => { - if (hasValue(formatResponse) && !formatResponse.isSuccessful) { + getFirstCompletedRemoteData(), + map((formatResponse: RemoteData) => { + if (hasValue(formatResponse) && formatResponse.hasFailed) { this.notificationsService.error( this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.format.title'), - formatResponse.statusText + formatResponse.errorMessage ); } else { - return this.bitstreamService.findById(this.bitstream.id).pipe( - getFirstSucceededRemoteDataPayload() - ); + return formatResponse.payload; } }) ); diff --git a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts index 0a681cce47..b9b2b8e11e 100644 --- a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { ChangeDetectorRef, Component } from '@angular/core'; import { BrowseByMetadataPageComponent, browseParamsToOptions @@ -43,7 +43,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { ngOnInit(): void { this.startsWithType = StartsWithType.date; - this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig)); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); this.subs.push( observableCombineLatest( this.route.params, diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts index 873f6ef464..1fcccb79c7 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts @@ -12,7 +12,7 @@ import { of as observableOf } from 'rxjs/internal/observable/of'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; @@ -157,5 +157,5 @@ describe('BrowseByMetadataPageComponent', () => { }); export function toRemoteData(objects: any[]): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), objects)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), objects)); } diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts index a2964e6abf..3b67d2e3d0 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -1,7 +1,7 @@ -import {combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { Component, OnInit } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { ActivatedRoute, Router } from '@angular/router'; @@ -10,10 +10,9 @@ import { BrowseService } from '../../core/browse/browse.service'; import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { Item } from '../../core/shared/item.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; -import { getSucceededRemoteData } from '../../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { take } from 'rxjs/operators'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator'; @@ -105,7 +104,7 @@ export class BrowseByMetadataPageComponent implements OnInit { } ngOnInit(): void { - this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig)); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); this.subs.push( observableCombineLatest( this.route.params, @@ -169,7 +168,7 @@ export class BrowseByMetadataPageComponent implements OnInit { updateParent(scope: string) { if (hasValue(scope)) { this.parent$ = this.dsoService.findById(scope).pipe( - getSucceededRemoteData() + getFirstSucceededRemoteData() ); } } @@ -179,11 +178,11 @@ export class BrowseByMetadataPageComponent implements OnInit { */ goPrev() { if (this.items$) { - this.items$.pipe(take(1)).subscribe((items) => { + this.items$.pipe(getFirstSucceededRemoteData()).subscribe((items) => { this.items$ = this.browseService.getPrevBrowseItems(items); }); } else if (this.browseEntries$) { - this.browseEntries$.pipe(take(1)).subscribe((entries) => { + this.browseEntries$.pipe(getFirstSucceededRemoteData()).subscribe((entries) => { this.browseEntries$ = this.browseService.getPrevBrowseEntries(entries); }); } @@ -194,11 +193,11 @@ export class BrowseByMetadataPageComponent implements OnInit { */ goNext() { if (this.items$) { - this.items$.pipe(take(1)).subscribe((items) => { + this.items$.pipe(getFirstSucceededRemoteData()).subscribe((items) => { this.items$ = this.browseService.getNextBrowseItems(items); }); } else if (this.browseEntries$) { - this.browseEntries$.pipe(take(1)).subscribe((entries) => { + this.browseEntries$.pipe(getFirstSucceededRemoteData()).subscribe((entries) => { this.browseEntries$ = this.browseService.getNextBrowseEntries(entries); }); } diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts index 91713cd219..da274fff1e 100644 --- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts @@ -32,7 +32,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { ngOnInit(): void { this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig)); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); this.subs.push( observableCombineLatest( this.route.params, diff --git a/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts b/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts index e64fa8a89d..fc8b40267a 100644 --- a/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts +++ b/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts @@ -6,7 +6,7 @@ import { Collection } from '../core/shared/collection.model'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { Observable } from 'rxjs'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../core/shared/operators'; import { map } from 'rxjs/operators'; import { hasValue } from '../shared/empty.util'; import { getDSORoute } from '../app-routing-paths'; @@ -29,7 +29,7 @@ export class BrowseByDSOBreadcrumbResolver { const uuid = route.queryParams.scope; if (hasValue(uuid)) { return this.dataService.findById(uuid).pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((object: Community | Collection) => { return { provider: this.breadcrumbService, key: object, url: getDSORoute(object) }; diff --git a/src/app/+browse-by/browse-by-guard.ts b/src/app/+browse-by/browse-by-guard.ts index 3ba282d41c..c734d30e49 100644 --- a/src/app/+browse-by/browse-by-guard.ts +++ b/src/app/+browse-by/browse-by-guard.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; import { map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { environment } from '../../environments/environment'; @@ -32,7 +32,7 @@ export class BrowseByGuard implements CanActivate { const value = route.queryParams.value; const metadataTranslated = this.translate.instant('browse.metadata.' + id); if (hasValue(scope)) { - const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getSucceededRemoteData()); + const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); return dsoAndMetadata$.pipe( map((dsoRD) => { const name = dsoRD.payload.name; diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index b0a93bd276..9a79ac9152 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -1,4 +1,3 @@ -import { filter, tap } from 'rxjs/operators'; import { CollectionItemMapperComponent } from './collection-item-mapper.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; @@ -22,8 +21,6 @@ import { EventEmitter } from '@angular/core'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { By } from '@angular/platform-browser'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; @@ -39,6 +36,12 @@ import { LoadingComponent } from '../../shared/loading/loading.component'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchService } from '../../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -60,7 +63,7 @@ describe('CollectionItemMapperComponent', () => { } } }); - const mockCollectionRD: RemoteData = new RemoteData(false, false, true, null, mockCollection); + const mockCollectionRD: RemoteData = createSuccessfulRemoteDataObject(mockCollection); const mockSearchOptions = of(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', @@ -81,7 +84,7 @@ describe('CollectionItemMapperComponent', () => { paginatedSearchOptions: mockSearchOptions }; const itemDataServiceStub = { - mapToCollection: () => of(new RestResponse(true, 200, 'OK')) + mapToCollection: () => createSuccessfulRemoteDataObject$({}) }; const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD }); const translateServiceStub = { @@ -90,7 +93,7 @@ describe('CollectionItemMapperComponent', () => { onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() }; - const emptyList = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); + const emptyList = createSuccessfulRemoteDataObject(createPaginatedList([])); const searchServiceStub = Object.assign(new SearchServiceStub(), { search: () => of(emptyList), /* tslint:disable:no-empty */ @@ -167,7 +170,7 @@ describe('CollectionItemMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(createFailedRemoteDataObject$('Not Found', 404)); comp.mapItems(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 84475c95eb..2fc999ad19 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -5,9 +5,13 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { map, startWith, switchMap, take } from 'rxjs/operators'; -import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; +import { + getRemoteDataPayload, + getFirstSucceededRemoteData, + toDSpaceObjectListRD +} from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; @@ -16,13 +20,13 @@ import { ItemDataService } from '../../core/data/item-data.service'; import { TranslateService } from '@ngx-translate/core'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { isNotEmpty } from '../../shared/empty.util'; -import { RestResponse } from '../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { SearchService } from '../../core/shared/search/search.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { NoContent } from '../../core/shared/NoContent.model'; @Component({ selector: 'ds-collection-item-mapper', @@ -102,7 +106,7 @@ export class CollectionItemMapperComponent implements OnInit { } ngOnInit(): void { - this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; + this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadItemLists(); } @@ -151,7 +155,7 @@ export class CollectionItemMapperComponent implements OnInit { */ mapItems(ids: string[], remove?: boolean) { const responses$ = this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((collectionRD: RemoteData) => collectionRD.payload), switchMap((collection: Collection) => observableCombineLatest(ids.map((id: string) => @@ -168,12 +172,12 @@ export class CollectionItemMapperComponent implements OnInit { * @param {Observable} responses$ The responses after adding/removing a mapping * @param {boolean} remove Whether or not the goal was to remove mappings */ - private showNotifications(responses$: Observable, remove?: boolean) { + private showNotifications(responses$: Observable>>, remove?: boolean) { const messageInsertion = remove ? 'unmap' : 'map'; - responses$.subscribe((responses: RestResponse[]) => { - const successful = responses.filter((response: RestResponse) => response.isSuccessful); - const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); + responses$.subscribe((responses: Array>) => { + const successful = responses.filter((response: RemoteData) => response.hasSucceeded); + const unsuccessful = responses.filter((response: RemoteData) => response.hasFailed); if (successful.length > 0) { const successMessages = observableCombineLatest( this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.success.head`), @@ -246,7 +250,7 @@ export class CollectionItemMapperComponent implements OnInit { */ onCancel() { this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), take(1) ).subscribe((collection: Collection) => { diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index c99ba34936..e5084cb61f 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -1,12 +1,12 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs'; -import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { filter, flatMap, map, startWith, switchMap, take } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { SearchService } from '../core/shared/search/search.service'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CollectionDataService } from '../core/data/collection-data.service'; -import { PaginatedList } from '../core/data/paginated-list'; +import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; import { MetadataService } from '../core/metadata/metadata.service'; @@ -16,13 +16,13 @@ import { Collection } from '../core/shared/collection.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { Item } from '../core/shared/item.model'; import { - getSucceededRemoteData, + getFirstSucceededRemoteData, redirectOn4xx, toDSpaceObjectListRD } from '../core/shared/operators'; import { fadeIn, fadeInOut } from '../shared/animations/fade'; -import { hasNoValue, hasValue, isNotEmpty } from '../shared/empty.util'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { AuthService } from '../core/auth/auth.service'; @@ -81,7 +81,7 @@ export class CollectionPageComponent implements OnInit { this.itemRD$ = this.paginationChanges$.pipe( switchMap((dto) => this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((rd) => rd.payload.id), switchMap((id: string) => { return this.searchService.search( diff --git a/src/app/+collection-page/collection-page.resolver.spec.ts b/src/app/+collection-page/collection-page.resolver.spec.ts index 5034b8d369..5ded339fb8 100644 --- a/src/app/+collection-page/collection-page.resolver.spec.ts +++ b/src/app/+collection-page/collection-page.resolver.spec.ts @@ -1,6 +1,6 @@ import { first } from 'rxjs/operators'; -import { of as observableOf } from 'rxjs'; import { CollectionPageResolver } from './collection-page.resolver'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; describe('CollectionPageResolver', () => { describe('resolve', () => { @@ -10,17 +10,18 @@ describe('CollectionPageResolver', () => { beforeEach(() => { collectionService = { - findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) + findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; resolver = new CollectionPageResolver(collectionService); }); - it('should resolve a collection with the correct id', () => { + it('should resolve a collection with the correct id', (done) => { resolver.resolve({ params: { id: uuid } } as any, undefined) .pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); + done(); } ); }); diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index 1c535e10aa..8377afacfc 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -4,9 +4,8 @@ import { Collection } from '../core/shared/collection.model'; import { Observable } from 'rxjs'; import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; -import { find } from 'rxjs/operators'; -import { hasValue } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * This class represents a resolver that requests a specific collection before the route is activated @@ -24,8 +23,8 @@ export class CollectionPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.collectionService.findById(route.params.id, followLink('logo')).pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + return this.collectionService.findById(route.params.id, false, followLink('logo')).pipe( + getFirstCompletedRemoteData() ); } } diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts index 6e8923ac93..91c2c47466 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts @@ -18,7 +18,7 @@ describe('CreateCollectionPageGuard', () => { } else if (id === 'invalid-id') { return createSuccessfulRemoteDataObject$(undefined); } else if (id === 'error-id') { - return createFailedRemoteDataObject$(new Community()); + return createFailedRemoteDataObject$('not found', 404); } } }; diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts index 5bcad1cbd7..ca84231912 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts @@ -5,8 +5,9 @@ import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { map, tap, find } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; /** * Prevent creation of a collection without a parent community provided @@ -30,7 +31,7 @@ export class CreateCollectionPageGuard implements CanActivate { } return this.communityService.findById(parentID) .pipe( - find((communityRD: RemoteData) => hasValue(communityRD.payload) || hasValue(communityRD.error)), + getFirstCompletedRemoteData(), map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), tap((isValid: boolean) => { if (!isValid) { diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index ca2a38aa86..b45b609731 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -7,7 +7,7 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { switchMap, take } from 'rxjs/operators'; import { combineLatest as combineLatestObservable } from 'rxjs'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -54,7 +54,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.findByCollectionID(collection.uuid)) ); @@ -65,13 +65,13 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.create(new Item(), collection.uuid)), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), take(1) ); @@ -86,13 +86,13 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.findByCollectionID(collection.uuid)), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), take(1) ); diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html index dc702ee61e..9c5020fc7f 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index b5e03b7983..5197a45926 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -11,6 +11,7 @@ import { SharedModule } from '../../../shared/shared.module'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { RequestService } from '../../../core/data/request.service'; import { RouterTestingModule } from '@angular/router/testing'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; describe('CollectionRolesComponent', () => { @@ -23,11 +24,7 @@ describe('CollectionRolesComponent', () => { const route = { parent: { data: observableOf({ - dso: new RemoteData( - false, - false, - true, - undefined, + dso: createSuccessfulRemoteDataObject( Object.assign(new Collection(), { _links: { irrelevant: { @@ -52,25 +49,18 @@ describe('CollectionRolesComponent', () => { }, ], }, - }), + }) ), }) } }; const requestService = { - hasByHrefObservable: () => observableOf(true), + hasByHref$: () => observableOf(true), }; const groupDataService = { - findByHref: () => observableOf(new RemoteData( - false, - false, - true, - undefined, - {}, - 200, - )), + findByHref: () => createSuccessfulRemoteDataObject$({}), }; TestBed.configureTestingModule({ diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts index 996933e43d..7912245869 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { HALLink } from '../../../core/shared/hal-link.model'; /** @@ -18,21 +18,33 @@ export class CollectionRolesComponent implements OnInit { dsoRD$: Observable>; - /** - * The collection to manage, as an observable. - */ - get collection$(): Observable { - return this.dsoRD$.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - ) - } - /** * The different roles for the collection, as an observable. */ - getComcolRoles(): Observable { - return this.collection$.pipe( + comcolRoles$: Observable + + /** + * The collection to manage, as an observable. + */ + collection$: Observable + + constructor( + protected route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe( + first(), + map((data) => data.dso), + ); + + this.collection$ = this.dsoRD$.pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + ); + + this.comcolRoles$ = this.collection$.pipe( map((collection) => [ { name: 'collection-admin', @@ -54,16 +66,4 @@ export class CollectionRolesComponent implements OnInit { ]), ); } - - constructor( - protected route: ActivatedRoute, - ) { - } - - ngOnInit(): void { - this.dsoRD$ = this.route.parent.data.pipe( - first(), - map((data) => data.dso), - ); - } } diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index be72fd7438..5c00cb35d2 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -17,9 +17,9 @@ import { FormControl, FormGroup } from '@angular/forms'; import { RouterStub } from '../../../shared/testing/router.stub'; import { By } from '@angular/platform-browser'; import { Collection } from '../../../core/shared/collection.model'; -import { RemoteData } from '../../../core/data/remote-data'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { RequestService } from '../../../core/data/request.service'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -111,7 +111,7 @@ describe('CollectionSourceComponent', () => { uuid: 'fake-collection-id' }); collectionService = jasmine.createSpyObj('collectionService', { - getContentSource: observableOf(contentSource), + getContentSource: createSuccessfulRemoteDataObject$(contentSource), updateContentSource: observableOf(contentSource), getHarvesterEndpoint: observableOf('harvester-endpoint') }); @@ -125,7 +125,7 @@ describe('CollectionSourceComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: Location, useValue: location }, { provide: DynamicFormService, useValue: formService }, - { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: new RemoteData(false, false, true, null, collection) }) } } }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: Router, useValue: router }, { provide: CollectionDataService, useValue: collectionService }, { provide: RequestService, useValue: requestService } diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 88b7501448..6ad6516f10 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -27,7 +27,7 @@ import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/obj import { Subscription } from 'rxjs/internal/Subscription'; import { cloneDeep } from 'lodash'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { MetadataConfig } from '../../../core/shared/metadata-config.model'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { RequestService } from '../../../core/data/request.service'; @@ -255,12 +255,12 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.getContentSource(uuid)), - take(1) - ).subscribe((contentSource: ContentSource) => { - this.initializeOriginalContentSource(contentSource); + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + this.initializeOriginalContentSource(rd.payload); }); this.updateFieldTranslations(); @@ -376,7 +376,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem onSubmit() { // Remove cached harvester request to allow for latest harvester to be displayed when switching tabs this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)), take(1) @@ -384,7 +384,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem // Update harvester this.collectionRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)), take(1) diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts index 885adadb3f..bd54de7b88 100644 --- a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -1,6 +1,6 @@ -import { of as observableOf } from 'rxjs/internal/observable/of'; import { first } from 'rxjs/operators'; import { ItemTemplatePageResolver } from './item-template-page.resolver'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; describe('ItemTemplatePageResolver', () => { describe('resolve', () => { @@ -10,17 +10,18 @@ describe('ItemTemplatePageResolver', () => { beforeEach(() => { itemTemplateService = { - findByCollectionID: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) + findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; resolver = new ItemTemplatePageResolver(itemTemplateService); }); - it('should resolve an item template with the correct id', () => { + it('should resolve an item template with the correct id', (done) => { resolver.resolve({ params: { id: uuid } } as any, undefined) .pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); + done(); } ); }); diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts index 919e131836..aeb92e9823 100644 --- a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -4,9 +4,8 @@ import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { Observable } from 'rxjs/internal/Observable'; -import { find } from 'rxjs/operators'; -import { hasValue } from '../../shared/empty.util'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; /** * This class represents a resolver that requests a specific collection's item template before the route is activated @@ -24,8 +23,8 @@ export class ItemTemplatePageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemTemplateService.findByCollectionID(route.params.id, followLink('templateItemOf')).pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + return this.itemTemplateService.findByCollectionID(route.params.id, false, followLink('templateItemOf')).pipe( + getFirstCompletedRemoteData(), ); } } diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index f65da14644..e4812e6514 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,8 +1,8 @@ import { mergeMap, filter, map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { CommunityDataService } from '../core/data/community-data.service'; import { RemoteData } from '../core/data/remote-data'; import { Bitstream } from '../core/shared/bitstream.model'; diff --git a/src/app/+community-page/community-page.resolver.spec.ts b/src/app/+community-page/community-page.resolver.spec.ts index aa6e9d9c1f..e75f5ad57e 100644 --- a/src/app/+community-page/community-page.resolver.spec.ts +++ b/src/app/+community-page/community-page.resolver.spec.ts @@ -1,6 +1,6 @@ -import { of as observableOf } from 'rxjs'; import { first } from 'rxjs/operators'; import { CommunityPageResolver } from './community-page.resolver'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; describe('CommunityPageResolver', () => { describe('resolve', () => { @@ -10,17 +10,18 @@ describe('CommunityPageResolver', () => { beforeEach(() => { communityService = { - findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) + findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; resolver = new CommunityPageResolver(communityService); }); - it('should resolve a community with the correct id', () => { + it('should resolve a community with the correct id', (done) => { resolver.resolve({ params: { id: uuid } } as any, undefined) .pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); + done(); } ); }); diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index 7696f7d469..0f5538eec9 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -4,9 +4,8 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { Community } from '../core/shared/community.model'; import { CommunityDataService } from '../core/data/community-data.service'; -import { find } from 'rxjs/operators'; -import { hasValue } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * This class represents a resolver that requests a specific community before the route is activated @@ -26,11 +25,12 @@ export class CommunityPageResolver implements Resolve> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.communityService.findById( route.params.id, + false, followLink('logo'), followLink('subcommunities'), followLink('collections') ).pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded) + getFirstCompletedRemoteData(), ); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts index 22e8812288..ac0b196c72 100644 --- a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts @@ -2,7 +2,10 @@ import { CreateCommunityPageGuard } from './create-community-page.guard'; import { RouterMock } from '../../shared/mocks/router.mock'; import { Community } from '../../core/shared/community.model'; import { first } from 'rxjs/operators'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; describe('CreateCommunityPageGuard', () => { describe('canActivate', () => { @@ -18,7 +21,7 @@ describe('CreateCommunityPageGuard', () => { } else if (id === 'invalid-id') { return createSuccessfulRemoteDataObject$(undefined); } else if (id === 'error-id') { - return createFailedRemoteDataObject$(new Community()); + return createFailedRemoteDataObject$('not found', 404); } } }; diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/+community-page/create-community-page/create-community-page.guard.ts index de7026c887..835fbb6589 100644 --- a/src/app/+community-page/create-community-page/create-community-page.guard.ts +++ b/src/app/+community-page/create-community-page/create-community-page.guard.ts @@ -5,8 +5,9 @@ import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { map, tap, find } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; /** * Prevent creation of a community with an invalid parent community provided @@ -30,7 +31,7 @@ export class CreateCommunityPageGuard implements CanActivate { return this.communityService.findById(parentID) .pipe( - find((communityRD: RemoteData) => hasValue(communityRD.payload) || hasValue(communityRD.error)), + getFirstCompletedRemoteData(), map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), tap((isValid: boolean) => { if (!isValid) { diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts index 4894046b10..87a1826a7d 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts @@ -11,6 +11,7 @@ import { RequestService } from '../../../core/data/request.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { SharedModule } from '../../../shared/shared.module'; import { RouterTestingModule } from '@angular/router/testing'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; describe('CommunityRolesComponent', () => { @@ -23,11 +24,7 @@ describe('CommunityRolesComponent', () => { const route = { parent: { data: observableOf({ - dso: new RemoteData( - false, - false, - true, - undefined, + dso: createSuccessfulRemoteDataObject( Object.assign(new Community(), { _links: { irrelevant: { @@ -37,25 +34,18 @@ describe('CommunityRolesComponent', () => { href: 'adminGroup link', }, }, - }), + }) ), }) } }; const requestService = { - hasByHrefObservable: () => observableOf(true), + hasByHref$: () => observableOf(true), }; const groupDataService = { - findByHref: () => observableOf(new RemoteData( - false, - false, - true, - undefined, - {}, - 200, - )), + findByHref: () => createSuccessfulRemoteDataObject$({}), }; TestBed.configureTestingModule({ diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts index 62e1f73bad..ec507907e3 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { Community } from '../../../core/shared/community.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { HALLink } from '../../../core/shared/hal-link.model'; @@ -23,7 +23,7 @@ export class CommunityRolesComponent implements OnInit { */ get community$(): Observable { return this.dsoRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ) } diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 060c5109b0..1f02858b6e 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -13,7 +13,7 @@ import { SharedModule } from '../../shared/shared.module'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { FindListOptions } from '../../core/data/request.models'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; @@ -105,7 +105,7 @@ describe('CommunityPageSubCollectionList Component', () => { if (endPageIndex > subCollList.length) { endPageIndex = subCollList.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); } }; diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index 64c274444e..261ae41aa2 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,16 +1,16 @@ import { Component, Input, OnInit } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; import { fadeIn } from '../../shared/animations/fade'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import { takeUntilCompletedRemoteData } from '../../core/shared/operators'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -72,7 +72,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } - }).pipe(take(1)).subscribe((results) => { + }).pipe(takeUntilCompletedRemoteData()).subscribe((results) => { this.subCollectionsRDObs.next(results); }); } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 363a6526d3..0c88a5c01d 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -9,7 +9,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; import { Community } from '../../core/shared/community.model'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { SharedModule } from '../../shared/shared.module'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -106,7 +106,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => { if (endPageIndex > subCommList.length) { endPageIndex = subCommList.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); } }; diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 1bd664021e..c9f72fbc04 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,15 +1,15 @@ import { Component, Input, OnInit } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; import { fadeIn } from '../../shared/animations/fade'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; +import { takeUntilCompletedRemoteData } from '../../core/shared/operators'; @Component({ selector: 'ds-community-page-sub-community-list', @@ -75,7 +75,7 @@ export class CommunityPageSubCommunityListComponent implements OnInit { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } - }).pipe(take(1)).subscribe((results) => { + }).pipe(takeUntilCompletedRemoteData()).subscribe((results) => { this.subCommunitiesRDObs.next(results); }); } diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts index 4ffa70ae8d..a97aac6b9e 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -9,7 +9,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TopLevelCommunityListComponent } from './top-level-community-list.component'; import { Community } from '../../core/shared/community.model'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { SharedModule } from '../../shared/shared.module'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -96,7 +96,7 @@ describe('TopLevelCommunityList Component', () => { if (endPageIndex > topCommList.length) { endPageIndex = topCommList.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); } }; diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 02c3cb54a0..b089244008 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,15 +1,15 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { hasValue } from '../../shared/empty.util'; /** * this component renders the Top-Level Community list @@ -22,7 +22,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c animations: [fadeInOut] }) -export class TopLevelCommunityListComponent implements OnInit { +export class TopLevelCommunityListComponent implements OnInit, OnDestroy { /** * A list of remote data objects of all top communities */ @@ -43,6 +43,11 @@ export class TopLevelCommunityListComponent implements OnInit { */ sortConfig: SortOptions; + /** + * The subscription to the observable for the current page. + */ + currentPageSubscription: Subscription; + constructor(private cds: CommunityDataService) { this.config = new PaginationComponentOptions(); this.config.id = this.pageId; @@ -71,12 +76,29 @@ export class TopLevelCommunityListComponent implements OnInit { * Update the list of top communities */ updatePage() { - this.cds.findTop({ + this.unsubscribe(); + this.currentPageSubscription = this.cds.findTop({ currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } - }).pipe(take(1)).subscribe((results) => { + }).subscribe((results) => { this.communitiesRD$.next(results); }); } + + /** + * Unsubscribe the top list subscription if it exists + */ + private unsubscribe() { + if (hasValue(this.currentPageSubscription)) { + this.currentPageSubscription.unsubscribe(); + } + } + + /** + * Clean up subscriptions when the component is destroyed + */ + ngOnDestroy() { + this.unsubscribe(); + } } diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts index b86a9e88cb..5d71b54741 100644 --- a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts @@ -16,7 +16,10 @@ import { Bitstream } from '../../../core/shared/bitstream.model'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { Bundle } from '../../../core/shared/bundle.model'; import { RequestService } from '../../../core/data/request.service'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RouterStub } from '../../../shared/testing/router.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; @@ -192,7 +195,7 @@ describe('UploadBistreamComponent', () => { function createUploadBitstreamTestingModule(queryParams) { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject(mockItem) + dso: createSuccessfulRemoteDataObject(mockItem) }), queryParams: observableOf(queryParams), snapshot: { diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts index eb42b1c30c..68d9ed7d8d 100644 --- a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts @@ -11,7 +11,7 @@ import { ItemDataService } from '../../../core/data/item-data.service'; import { AuthService } from '../../../core/auth/auth.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { @@ -103,7 +103,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.itemId = this.route.snapshot.params.id; - this.itemRD$ = this.route.data.pipe(map((data) => data.item)); + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)); this.bundlesRD$ = this.itemRD$.pipe( switchMap((itemRD: RemoteData) => itemRD.payload.bundles) ); diff --git a/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts index 98e2b77466..e7bd5b98ce 100644 --- a/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts +++ b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts @@ -6,6 +6,7 @@ import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { isNotEmpty } from '../../shared/empty.util'; describe('findSuccessfulAccordingTo', () => { let mockItem1; @@ -19,12 +20,12 @@ describe('findSuccessfulAccordingTo', () => { mockItem2 = new Item(); mockItem1.isWithdrawn = false; - predicate = (rd: RemoteData) => rd.payload.isWithdrawn; + predicate = (rd: RemoteData) => isNotEmpty(rd.payload) ? rd.payload.isWithdrawn : false; }); it('should return first successful RemoteData Observable that complies to predicate', () => { const testRD = { a: createSuccessfulRemoteDataObject(undefined), - b: createFailedRemoteDataObject(mockItem1), + b: createFailedRemoteDataObject(), c: createSuccessfulRemoteDataObject(mockItem2), d: createSuccessfulRemoteDataObject(mockItem1), e: createSuccessfulRemoteDataObject(mockItem2), diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index dcf70a30cb..915d4e17e5 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -2,21 +2,18 @@ import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing' import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - import { of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; - import { ItemAuthorizationsComponent } from './item-authorizations.component'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bundle } from '../../../core/shared/bundle.model'; -import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec'; import { Item } from '../../../core/shared/item.model'; import { LinkService } from '../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { createTestComponent } from '../../../shared/testing/utils.test'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { PageInfo } from '../../../core/shared/page-info.model'; describe('ItemAuthorizationsComponent test suite', () => { @@ -49,7 +46,7 @@ describe('ItemAuthorizationsComponent test suite', () => { _links: { self: { href: 'bundle1-selflink' } }, - bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) + bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])) }); const bundle2 = Object.assign(new Bundle(), { id: 'bundle2', @@ -57,11 +54,11 @@ describe('ItemAuthorizationsComponent test suite', () => { _links: { self: { href: 'bundle2-selflink' } }, - bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4]) + bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4])) }); const bundles = [bundle1, bundle2]; - const bitstreamList1: PaginatedList = new PaginatedList(new PageInfo(), [bitstream1, bitstream2]); - const bitstreamList2: PaginatedList = new PaginatedList(new PageInfo(), [bitstream3, bitstream4]); + const bitstreamList1: PaginatedList = buildPaginatedList(new PageInfo(), [bitstream1, bitstream2]); + const bitstreamList2: PaginatedList = buildPaginatedList(new PageInfo(), [bitstream3, bitstream4]); const item = Object.assign(new Item(), { uuid: 'item', @@ -69,7 +66,7 @@ describe('ItemAuthorizationsComponent test suite', () => { _links: { self: { href: 'item-selflink' } }, - bundles: createMockRDPaginatedObs([bundle1, bundle2]) + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([bundle1, bundle2])) }); const routeStub = { diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 8b89de7c89..c5d5817c33 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload @@ -89,7 +89,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataWithNotEmptyPayload(), catchError((error) => { console.error(error); - return observableOf(new PaginatedList(null, [])) + return observableOf(buildPaginatedList(null, [])) }) ); @@ -139,7 +139,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataPayload(), catchError((error) => { console.error(error); - return observableOf(new PaginatedList(null, [])) + return observableOf(buildPaginatedList(null, [])) }) ) } diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 5f6e3a06c4..1a8aafd666 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -1,8 +1,5 @@ import { Bitstream } from '../../../core/shared/bitstream.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { PageInfo } from '../../../core/shared/page-info.model'; import { Item } from '../../../core/shared/item.model'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ItemBitstreamsComponent } from './item-bitstreams.component'; @@ -13,7 +10,10 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; -import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { + INotification, + Notification +} from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -26,6 +26,11 @@ import { RestResponse } from '../../../core/cache/response.models'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { RouterStub } from '../../../shared/testing/router.stub'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -55,7 +60,7 @@ const bundle = Object.assign(new Bundle(), { _links: { self: { href: 'bundle1-selflink' } }, - bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) + bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])) }); const moveOperations = [ { @@ -130,17 +135,17 @@ describe('ItemBitstreamsComponent', () => { _links: { self: { href: 'item-selflink' } }, - bundles: createMockRDPaginatedObs([bundle]), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([bundle])), lastModified: date }); itemService = Object.assign( { - getBitstreams: () => createMockRDPaginatedObs([bitstream1, bitstream2]), - findById: () => createMockRDObs(item), - getBundles: () => createMockRDPaginatedObs([bundle]) + getBitstreams: () => createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])), + findById: () => createSuccessfulRemoteDataObject$(item), + getBundles: () => createSuccessfulRemoteDataObject$(createPaginatedList([bundle])) }); route = Object.assign({ parent: { - data: observableOf({ dso: createMockRD(item) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) }, data: observableOf({}), url: url @@ -235,15 +240,3 @@ describe('ItemBitstreamsComponent', () => { }); }); }); - -export function createMockRDPaginatedObs(list: any[]) { - return createMockRDObs(new PaginatedList(new PageInfo(), list)); -} - -export function createMockRDObs(obj: any) { - return observableOf(createMockRD(obj)); -} - -export function createMockRD(obj: any) { - return new RemoteData(false, false, true, null, obj); -} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index e00f4ae4aa..80daf4cf77 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -11,13 +11,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { zip as observableZip, of as observableOf } from 'rxjs'; -import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } 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'; +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 { Bitstream } from '../../../core/shared/bitstream.model'; @@ -26,6 +25,8 @@ import { BundleDataService } from '../../../core/data/bundle-data.service'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { Operation } from 'fast-json-patch'; @Component({ selector: 'ds-item-bitstreams', @@ -107,7 +108,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ postItemInit(): void { this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page) ); @@ -125,10 +126,10 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme * Also re-initialize the original fields and updates */ initializeItemUpdate(): void { - this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( + this.itemUpdateSubscription = this.requestService.hasByHref$(this.item.self).pipe( filter((exists: boolean) => !exists), switchMap(() => this.itemService.findById(this.item.uuid)), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), ).subscribe((itemRD: RemoteData) => { if (hasValue(itemRD)) { this.item = itemRD.payload; @@ -173,7 +174,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme ); // Perform the setup actions from above in order and display notifications - removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => { + removedResponses$.pipe(take(1)).subscribe((responses: Array>) => { this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); this.reset(); this.submitting = false; @@ -190,12 +191,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme dropBitstream(bundle: Bundle, event: any) { this.zone.runOutsideAngular(() => { if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { - const moveOperation = Object.assign({ + const moveOperation = { op: 'move', from: `/_links/bitstreams/${event.fromIndex}/href`, path: `/_links/bitstreams/${event.toIndex}/href` - }); - this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => { + } as Operation; + this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { this.zone.run(() => { this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); // Remove all cached requests from this bundle and call the event's callback when the requests are cleared @@ -216,12 +217,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme * @param key The i18n key for the notification messages * @param responses The returned responses to display notifications for */ - displayNotifications(key: string, responses: RestResponse[]) { + displayNotifications(key: string, responses: Array>) { if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RestResponse) => hasValue(response) && !response.isSuccessful); - const successfulResponses = responses.filter((response: RestResponse) => hasValue(response) && response.isSuccessful); + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); - failedResponses.forEach((response: ErrorResponse) => { + failedResponses.forEach((response: RemoteData) => { this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); }); if (successfulResponses.length > 0) { diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts index 118f2b1619..28ef32610e 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts @@ -7,7 +7,6 @@ import { VarDirective } from '../../../../../shared/utils/var.directive'; import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { createMockRDObs } from '../../item-bitstreams.component.spec'; import { Bitstream } from '../../../../../core/shared/bitstream.model'; import { BitstreamFormat } from '../../../../../core/shared/bitstream-format.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; @@ -49,7 +48,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { name: 'Fake Bitstream 1', bundleName: 'ORIGINAL', description: 'Description', - format: createMockRDObs(format) + format: createSuccessfulRemoteDataObject$(format) }); const fieldUpdate1 = { field: bitstream1, @@ -60,7 +59,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { name: 'Fake Bitstream 2', bundleName: 'ORIGINAL', description: 'Description', - format: createMockRDObs(format) + format: createSuccessfulRemoteDataObject$(format) }); const fieldUpdate2 = { field: bitstream2, @@ -107,7 +106,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { objectValuesPipe = new ObjectValuesPipe(); requestService = jasmine.createSpyObj('requestService', { - hasByHrefObservable: observableOf(true) + hasByHref$: observableOf(true) }); TestBed.configureTestingModule({ diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts index a288e9993a..bb77bfc0c4 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -53,7 +53,7 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate switchMap((page: number) => { const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}); return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( - switchMap((href) => this.requestService.hasByHrefObservable(href)), + switchMap((href) => this.requestService.hasByHref$(href)), switchMap(() => this.bundleService.getBitstreams( this.bundle.id, paginatedOptions, diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts index 30b5e0d376..e6da42931a 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts @@ -6,10 +6,10 @@ import { Bitstream } from '../../../../core/shared/bitstream.model'; import { TranslateModule } from '@ngx-translate/core'; import { VarDirective } from '../../../../shared/utils/var.directive'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { createMockRDObs } from '../item-bitstreams.component.spec'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; let comp: ItemEditBitstreamComponent; let fixture: ComponentFixture; @@ -29,7 +29,7 @@ const bitstream = Object.assign(new Bitstream(), { name: 'Fake Bitstream', bundleName: 'ORIGINAL', description: 'Description', - format: createMockRDObs(format) + format: createSuccessfulRemoteDataObject$(format) }); const fieldUpdate = { field: bitstream, diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts index 5a02b9cac4..5f97766557 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts @@ -6,7 +6,7 @@ import { ObjectUpdatesService } from '../../../../core/data/object-updates/objec import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { Observable } from 'rxjs/internal/Observable'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @@ -74,7 +74,7 @@ export class ItemEditBitstreamComponent implements OnChanges, OnInit { this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream; this.bitstreamName = this.dsoNameService.getName(this.bitstream); this.format$ = this.bitstream.format.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload() ); } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 9aeb1522a6..6ee630572a 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -12,11 +12,9 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti import { RestResponse } from '../../../core/cache/response.models'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; -import { PageInfo } from '../../../core/shared/page-info.model'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SearchService } from '../../../core/shared/search/search.service'; import { ErrorComponent } from '../../../shared/error/error.component'; @@ -38,6 +36,12 @@ import { SearchServiceStub } from '../../../shared/testing/search-service.stub'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { ItemCollectionMapperComponent } from './item-collection-mapper.component'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; @@ -55,7 +59,7 @@ describe('ItemCollectionMapperComponent', () => { id: '932c7d50-d85a-44cb-b9dc-b427b12877bd', name: 'test-item' }); - const mockItemRD: RemoteData = new RemoteData(false, false, true, null, mockItem); + const mockItemRD: RemoteData = createSuccessfulRemoteDataObject(mockItem); const mockSearchOptions = of(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', @@ -74,10 +78,10 @@ describe('ItemCollectionMapperComponent', () => { const searchConfigServiceStub = { paginatedSearchOptions: mockSearchOptions }; - const mockCollectionsRD = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); + const mockCollectionsRD = createSuccessfulRemoteDataObject(createPaginatedList([])); const itemDataServiceStub = { - mapToCollection: () => of(new RestResponse(true, 200, 'OK')), - removeMappingFromCollection: () => of(new RestResponse(true, 200, 'OK')), + mapToCollection: () => createSuccessfulRemoteDataObject$({}), + removeMappingFromCollection: () => createSuccessfulRemoteDataObject$({}), getMappedCollections: () => of(mockCollectionsRD), /* tslint:disable:no-empty */ clearMappedCollectionsRequests: () => {} @@ -143,7 +147,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(createFailedRemoteDataObject$('Not Found', 404)); comp.mapCollections(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); @@ -160,7 +164,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if the removal of at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); + spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(createFailedRemoteDataObject$('Not Found', 404)); comp.removeMappings(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index df406f826b..03328f18c2 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -4,13 +4,13 @@ import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/ import { CollectionDataService } from '../../../core/data/collection-data.service'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload, getRemoteDataPayload, - getSucceededRemoteData, + getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; @@ -20,11 +20,11 @@ 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 { RestResponse } from '../../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; 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'; +import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ selector: 'ds-item-collection-mapper', @@ -92,7 +92,7 @@ export class ItemCollectionMapperComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadCollectionLists(); } @@ -141,13 +141,13 @@ export class ItemCollectionMapperComponent implements OnInit { mapCollections(ids: string[]) { const itemIdAndExcludingIds$ = observableCombineLatest( this.itemRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), take(1), map((rd: RemoteData) => rd.payload), map((item: Item) => item.id) ), this.itemCollectionsRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), take(1), map((rd: RemoteData>) => rd.payload.page), map((collections: Collection[]) => collections.map((collection: Collection) => collection.id)) @@ -168,7 +168,7 @@ export class ItemCollectionMapperComponent implements OnInit { */ removeMappings(ids: string[]) { const responses$ = this.itemRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), map((itemRD: RemoteData) => itemRD.payload.id), switchMap((itemId: string) => observableCombineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id)))) ); @@ -191,10 +191,10 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {Observable} responses$ The responses after adding/removing a mapping * @param {string} messagePrefix The prefix to build the notification messages with */ - private showNotifications(responses$: Observable, messagePrefix: string) { - responses$.subscribe((responses: RestResponse[]) => { - const successful = responses.filter((response: RestResponse) => response.isSuccessful); - const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); + private showNotifications(responses$: Observable>>, messagePrefix: string) { + responses$.subscribe((responses: Array>) => { + const successful = responses.filter((response: RemoteData) => response.hasSucceeded); + const unsuccessful = responses.filter((response: RemoteData) => response.hasFailed); if (successful.length > 0) { const successMessages = observableCombineLatest( this.translateService.get(`${messagePrefix}.success.head`), @@ -280,7 +280,7 @@ export class ItemCollectionMapperComponent implements OnInit { */ onCancel() { this.itemRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), take(1) ).subscribe((item: Item) => { diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index e7b454e92b..53ae74a96c 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -16,16 +16,17 @@ import { NotificationsService } from '../../../shared/notifications/notification import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ItemDeleteComponent } from './item-delete.component'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; import { VarDirective } from '../../../shared/utils/var.directive'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { RelationshipService } from '../../../core/data/relationship.service'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { PageInfo } from '../../../core/shared/page-info.model'; import { EntityTypeService } from '../../../core/data/entity-type.service'; import { getItemEditRoute } from '../../item-page-routing-paths'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; let comp: ItemDeleteComponent; let fixture: ComponentFixture; @@ -78,52 +79,16 @@ describe('ItemDeleteComponent', () => { Object.assign(new Relationship(), { id: '1', uuid: 'relationship-1', - relationshipType: observableOf(new RemoteData( - false, - false, - true, - null, - type1 - )), - leftItem: observableOf(new RemoteData( - false, - false, - true, - null, - mockItem, - )), - rightItem: observableOf(new RemoteData( - false, - false, - true, - null, - Object.assign(new Item(), {}) - )), + relationshipType: createSuccessfulRemoteDataObject$(type1), + leftItem: createSuccessfulRemoteDataObject$(mockItem), + rightItem: createSuccessfulRemoteDataObject$(new Item()), }), Object.assign(new Relationship(), { id: '2', uuid: 'relationship-2', - relationshipType: observableOf(new RemoteData( - false, - false, - true, - null, - type2 - )), - leftItem: observableOf(new RemoteData( - false, - false, - true, - null, - mockItem, - )), - rightItem: observableOf(new RemoteData( - false, - false, - true, - null, - Object.assign(new Item(), {}) - )), + relationshipType: createSuccessfulRemoteDataObject$(type2), + leftItem: createSuccessfulRemoteDataObject$(mockItem), + rightItem: createSuccessfulRemoteDataObject$(new Item()), }), ]; @@ -133,7 +98,7 @@ describe('ItemDeleteComponent', () => { }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { - delete: observableOf(true) + delete: createSuccessfulRemoteDataObject$({}) }); routeStub = { @@ -149,20 +114,8 @@ describe('ItemDeleteComponent', () => { entityTypeService = jasmine.createSpyObj('entityTypeService', { - getEntityTypeByLabel: observableOf(new RemoteData( - false, - false, - true, - null, - itemType, - )), - getEntityTypeRelationships: observableOf(new RemoteData( - false, - false, - true, - null, - new PaginatedList(new PageInfo(), types), - )), + getEntityTypeByLabel: createSuccessfulRemoteDataObject$(itemType), + getEntityTypeRelationships: createSuccessfulRemoteDataObject$(createPaginatedList(types)), } ); @@ -236,5 +189,4 @@ describe('ItemDeleteComponent', () => { expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('fake-id')]); }); }); -}) -; +}); diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index ac73c561b2..ffe71948d5 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,12 +1,21 @@ import { Component, Input, OnInit } from '@angular/core'; -import { defaultIfEmpty, filter, first, map, switchMap, take } from 'rxjs/operators'; +import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf } from 'rxjs'; +import { + combineLatest as observableCombineLatest, + combineLatest, + Observable, + of as observableOf +} from 'rxjs'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { + getRemoteDataPayload, + getFirstSucceededRemoteData, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { Item } from '../../../core/shared/item.model'; import { MetadataValue } from '../../../core/shared/metadata.models'; @@ -20,8 +29,9 @@ import { RelationshipService } from '../../../core/data/relationship.service'; import { EntityTypeService } from '../../../core/data/entity-type.service'; import { LinkService } from '../../../core/cache/builders/link.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { RestResponse } from '../../../core/cache/response.models'; import { getItemEditRoute } from '../../item-page-routing-paths'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ selector: 'ds-item-delete', @@ -105,10 +115,10 @@ export class ItemDeleteComponent const label = this.item.firstMetadataValue('relationship.type'); if (label !== undefined) { this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((relationshipTypes) => relationshipTypes.page), switchMap((types) => @@ -220,7 +230,7 @@ export class ItemDeleteComponent followLink('rightItem'), ); return relationship.relationshipType.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), filter((relationshipType: RelationshipType) => hasValue(relationshipType) && isNotEmpty(relationshipType.uuid)) ); @@ -238,7 +248,7 @@ export class ItemDeleteComponent relationship, this.isLeftItem(relationship).pipe( switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ), ); @@ -285,7 +295,7 @@ export class ItemDeleteComponent private isLeftItem(relationship: Relationship): Observable { return relationship.leftItem.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)), map((leftItem) => leftItem.uuid === this.item.uuid) @@ -327,9 +337,9 @@ export class ItemDeleteComponent ) ), ).subscribe((types) => { - this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe( - (response: RestResponse) => { - this.notify(response.isSuccessful); + this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()).subscribe( + (rd: RemoteData) => { + this.notify(rd.hasSucceeded); } ); }); diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 4ecdb21e24..709e451bd3 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -9,7 +9,7 @@ import { TestScheduler } from 'rxjs/testing'; import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { RegistryService } from '../../../../core/registry/registry.service'; @@ -66,7 +66,7 @@ describe('EditInPlaceFieldComponent', () => { beforeEach(async(() => { scheduler = getTestScheduler(); - paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]); + paginatedMetadataFields = buildPaginatedList(undefined, [mdField1, mdField2, mdField3]); metadataFieldService = jasmine.createSpyObj({ queryMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields), @@ -221,7 +221,7 @@ describe('EditInPlaceFieldComponent', () => { })); it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { - expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, followLink('schema')); + expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, false, followLink('schema')); }); it('it should set metadataFieldSuggestions to the right value', () => { diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 8543bbef42..7c2ffecb0a 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -1,10 +1,13 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { metadataFieldsToString } from '../../../../core/shared/operators'; +import { + metadataFieldsToString, + getFirstSucceededRemoteData +} from '../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; @@ -124,10 +127,10 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { */ findMetadataFieldSuggestions(query: string) { if (isNotEmpty(query)) { - return this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe( + return this.registryService.queryMetadataFields(query, null, false, followLink('schema')).pipe( + getFirstSucceededRemoteData(), metadataFieldsToString(), - take(1)) - .subscribe((fieldNames: string[]) => { + ).subscribe((fieldNames: string[]) => { this.setInputSuggestions(fieldNames); }) } else { diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index f30b5cc3b0..62c1aa70d1 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -21,7 +21,6 @@ import { Item } from '../../../core/shared/item.model'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { MetadatumViewModel } from '../../../core/shared/metadata.models'; import { RegistryService } from '../../../core/registry/registry.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { @@ -30,6 +29,7 @@ import { } from '../../../shared/remote-data.utils'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { DSOSuccessResponse } from '../../../core/cache/response.models'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; let comp: any; let fixture: ComponentFixture; @@ -133,7 +133,7 @@ describe('ItemMetadataComponent', () => { data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } }; - paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]); + paginatedMetadataFields = createPaginatedList([mdField1, mdField2, mdField3]); metadataFieldService = jasmine.createSpyObj({ getAllMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields) diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 5ad6254459..ea095e0e01 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -4,19 +4,18 @@ 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 { cloneDeep } from 'lodash'; -import { first, switchMap, tap } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { first, switchMap } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { UpdateDataService } from '../../../core/data/update-data.service'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { AlertType } from '../../../shared/alert/aletr-type'; import { Operation } from 'fast-json-patch'; -import { METADATA_PATCH_OPERATION_SERVICE_TOKEN } from '../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service'; -import { DSOSuccessResponse, ErrorResponse } from '../../../core/cache/response.models'; +import { MetadataPatchOperationService } from '../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service'; @Component({ selector: 'ds-item-metadata', @@ -87,7 +86,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, METADATA_PATCH_OPERATION_SERVICE_TOKEN); + this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, MetadataPatchOperationService); } /** @@ -101,26 +100,20 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { first(), switchMap((patch: Operation[]) => { return this.updateService.patch(this.item, patch).pipe( - tap((response) => { - if (!response.isSuccessful) { - this.notificationsService.error(this.getNotificationTitle('error'), (response as ErrorResponse).errorMessage); - } - }), - switchMap((response: DSOSuccessResponse) => { - if (isNotEmpty(response.resourceSelfLinks)) { - return this.itemService.findByHref(response.resourceSelfLinks[0]); - } - }), - getSucceededRemoteData() + getFirstCompletedRemoteData() ); }) ).subscribe( (rd: RemoteData) => { - this.item = rd.payload; - this.checkAndFixMetadataUUIDs(); - this.initializeOriginalFields(); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + if (rd.hasFailed) { + this.notificationsService.error(this.getNotificationTitle('error'), rd.errorMessage); + } else { + this.item = rd.payload; + this.checkAndFixMetadataUUIDs(); + this.initializeOriginalFields(); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + } } ) } else { diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index c8c49b118b..7f9b53f7ba 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -7,10 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../core/cache/response.models'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { SearchService } from '../../../core/shared/search/search.service'; @@ -18,6 +15,12 @@ import { NotificationsService } from '../../../shared/notifications/notification import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../../shared/testing/router.stub'; import { ItemMoveComponent } from './item-move.component'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; describe('ItemMoveComponent', () => { let comp: ItemMoveComponent; @@ -34,22 +37,6 @@ describe('ItemMoveComponent', () => { url: `${itemPageUrl}/edit` }); - const mockItemDataService = jasmine.createSpyObj({ - moveToCollection: observableOf(new RestResponse(true, 200, 'Success')) - }); - - const mockItemDataServiceFail = jasmine.createSpyObj({ - moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error')) - }); - - const routeStub = { - data: observableOf({ - dso: new RemoteData(false, false, true, null, { - id: 'item1' - }) - }) - }; - const collection1 = Object.assign(new Collection(), { uuid: 'collection-uuid-1', name: 'Test collection 1' @@ -60,18 +47,33 @@ describe('ItemMoveComponent', () => { name: 'Test collection 2' }); + const mockItemDataService = jasmine.createSpyObj({ + moveToCollection: createSuccessfulRemoteDataObject$(collection1) + }); + + const mockItemDataServiceFail = jasmine.createSpyObj({ + moveToCollection: createFailedRemoteDataObject$('Internal server error', 500) + }); + + const routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject({ + id: 'item1' + }) + }) + }; + const mockSearchService = { search: () => { - return observableOf(new RemoteData(false, false, true, null, - new PaginatedList(null, [ - { - indexableObject: collection1, - hitHighlights: {} - }, { - indexableObject: collection2, - hitHighlights: {} - } - ]))); + return createSuccessfulRemoteDataObject$(createPaginatedList([ + { + indexableObject: collection1, + hitHighlights: {} + }, { + indexableObject: collection2, + hitHighlights: {} + } + ])); } }; diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 1a544af7dc..cdff5e1fbc 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -3,15 +3,17 @@ import { first, map } from 'rxjs/operators'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { RemoteData } from '../../../core/data/remote-data'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteData, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Observable, of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../core/cache/response.models'; import { Collection } from '../../../core/shared/collection.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SearchService } from '../../../core/shared/search/search.service'; @@ -55,7 +57,7 @@ export class ItemMoveComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable>; this.itemRD$.subscribe((rd) => { this.itemId = rd.payload.id; } @@ -114,10 +116,10 @@ export class ItemMoveComponent implements OnInit { */ moveCollection() { this.processing = true; - this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(first()).subscribe( - (response: RestResponse) => { + this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe( + (response: RemoteData) => { this.router.navigate([getItemEditRoute(this.itemId)]); - if (response.isSuccessful) { + if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get('item.edit.move.success')); } else { this.notificationsService.error(this.translateService.get('item.edit.move.error')); diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts index 52ccbc2133..d8a615d218 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts @@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Item } from '../../../core/shared/item.model'; import { RouterStub } from '../../../shared/testing/router.stub'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -16,7 +15,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ItemPrivateComponent } from './item-private.component'; import { RestResponse } from '../../../core/cache/response.models'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; let comp: ItemPrivateComponent; let fixture: ComponentFixture; @@ -46,14 +45,12 @@ describe('ItemPrivateComponent', () => { }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { - setDiscoverable: observableOf(new RestResponse(true, 200, 'OK')) + setDiscoverable: createSuccessfulRemoteDataObject$(mockItem) }); routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ - id: 'fake-id' - }) + dso: createSuccessfulRemoteDataObject(mockItem) }) }; @@ -98,9 +95,8 @@ describe('ItemPrivateComponent', () => { spyOn(comp, 'processRestResponse'); comp.performAction(); - expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, false); + expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem, false); expect(comp.processRestResponse).toHaveBeenCalled(); }); }); -}) -; +}); diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts index d949e4fa6e..02a7f997bf 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; -import { first } from 'rxjs/operators'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { RestResponse } from '../../../core/cache/response.models'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-private', @@ -21,9 +20,9 @@ export class ItemPrivateComponent extends AbstractSimpleItemActionComponent { * Perform the make private action to the item */ performAction() { - this.itemDataService.setDiscoverable(this.item.id, false).pipe(first()).subscribe( - (response: RestResponse) => { - this.processRestResponse(response); + this.itemDataService.setDiscoverable(this.item, false).pipe(getFirstCompletedRemoteData()).subscribe( + (rd: RemoteData) => { + this.processRestResponse(rd); } ); } diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts index 1143874709..dbc948de8c 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts @@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Item } from '../../../core/shared/item.model'; import { RouterStub } from '../../../shared/testing/router.stub'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -15,8 +14,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ItemPublicComponent } from './item-public.component'; -import { RestResponse } from '../../../core/cache/response.models'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; let comp: ItemPublicComponent; let fixture: ComponentFixture; @@ -27,8 +25,6 @@ let routerStub; let mockItemDataService: ItemDataService; let routeStub; let notificationsServiceStub; -let successfulRestResponse; -let failRestResponse; describe('ItemPublicComponent', () => { beforeEach(async(() => { @@ -46,14 +42,12 @@ describe('ItemPublicComponent', () => { }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { - setDiscoverable: observableOf(new RestResponse(true, 200, 'OK')) + setDiscoverable: createSuccessfulRemoteDataObject$(mockItem) }); routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ - id: 'fake-id' - }) + dso: createSuccessfulRemoteDataObject(mockItem) }) }; @@ -74,9 +68,6 @@ describe('ItemPublicComponent', () => { })); beforeEach(() => { - successfulRestResponse = new RestResponse(true, 200, 'OK'); - failRestResponse = new RestResponse(false, 500, 'Internal Server Error'); - fixture = TestBed.createComponent(ItemPublicComponent); comp = fixture.componentInstance; fixture.detectChanges(); @@ -98,9 +89,8 @@ describe('ItemPublicComponent', () => { spyOn(comp, 'processRestResponse'); comp.performAction(); - expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, true); + expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem, true); expect(comp.processRestResponse).toHaveBeenCalled(); }); }); -}) -; +}); diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts index 272cf9a96f..dcadc8bac3 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; -import { first } from 'rxjs/operators'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { RestResponse } from '../../../core/cache/response.models'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-public', @@ -21,8 +20,8 @@ export class ItemPublicComponent extends AbstractSimpleItemActionComponent { * Perform the make public action to the item */ performAction() { - this.itemDataService.setDiscoverable(this.item.id, true).pipe(first()).subscribe( - (response: RestResponse) => { + this.itemDataService.setDiscoverable(this.item, true).pipe(getFirstCompletedRemoteData()).subscribe( + (response: RemoteData) => { this.processRestResponse(response); } ); diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index 005f330df9..e6cf6bc374 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Item } from '../../../core/shared/item.model'; import { RouterStub } from '../../../shared/testing/router.stub'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -15,8 +14,10 @@ import { NotificationsService } from '../../../shared/notifications/notification import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ItemReinstateComponent } from './item-reinstate.component'; -import { RestResponse } from '../../../core/cache/response.models'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; let comp: ItemReinstateComponent; let fixture: ComponentFixture; @@ -27,8 +28,6 @@ let routerStub; let mockItemDataService: ItemDataService; let routeStub; let notificationsServiceStub; -let successfulRestResponse; -let failRestResponse; describe('ItemReinstateComponent', () => { beforeEach(async(() => { @@ -46,7 +45,7 @@ describe('ItemReinstateComponent', () => { }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { - setWithDrawn: observableOf(new RestResponse(true, 200, 'OK')) + setWithDrawn: createSuccessfulRemoteDataObject$(mockItem) }); routeStub = { @@ -74,9 +73,6 @@ describe('ItemReinstateComponent', () => { })); beforeEach(() => { - successfulRestResponse = new RestResponse(true, 200, 'OK'); - failRestResponse = new RestResponse(false, 500, 'Internal Server Error'); - fixture = TestBed.createComponent(ItemReinstateComponent); comp = fixture.componentInstance; fixture.detectChanges(); @@ -98,9 +94,8 @@ describe('ItemReinstateComponent', () => { spyOn(comp, 'processRestResponse'); comp.performAction(); - expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, false); + expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(comp.item, false); expect(comp.processRestResponse).toHaveBeenCalled(); }); }); -}) -; +}); diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts index 9c0e1c8d05..cfebcb16f1 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; -import { first } from 'rxjs/operators'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { RestResponse } from '../../../core/cache/response.models'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-reinstate', @@ -21,8 +20,8 @@ export class ItemReinstateComponent extends AbstractSimpleItemActionComponent { * Perform the reinstate action to the item */ performAction() { - this.itemDataService.setWithDrawn(this.item.id, false).pipe(first()).subscribe( - (response: RestResponse) => { + this.itemDataService.setWithDrawn(this.item, false).pipe(getFirstCompletedRemoteData()).subscribe( + (response: RemoteData) => { this.processRestResponse(response); } ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index cd583fd22b..009f1e7fc5 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -6,19 +6,16 @@ import { of as observableOf } from 'rxjs/internal/observable/of'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; -import { RelationshipTypeService } from '../../../../core/data/relationship-type.service'; import { RelationshipService } from '../../../../core/data/relationship.service'; -import { RemoteData } from '../../../../core/data/remote-data'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { getMockLinkService } from '../../../../shared/mocks/link-service.mock'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { SharedModule } from '../../../../shared/shared.module'; import { EditRelationshipListComponent } from './edit-relationship-list.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; let comp: EditRelationshipListComponent; let fixture: ComponentFixture; @@ -60,20 +57,8 @@ describe('EditRelationshipListComponent', () => { relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftType: observableOf(new RemoteData( - false, - false, - true, - undefined, - entityType, - )), - rightType: observableOf(new RemoteData( - false, - false, - true, - undefined, - relatedEntityType, - )), + leftType: createSuccessfulRemoteDataObject$(entityType), + rightType: createSuccessfulRemoteDataObject$(relatedEntityType), leftwardType: 'isAuthorOfPublication', rightwardType: 'isPublicationOfAuthor', }); @@ -92,53 +77,17 @@ describe('EditRelationshipListComponent', () => { self: url + '/2', id: '2', uuid: '2', - relationshipType: observableOf(new RemoteData( - false, - false, - true, - undefined, - relationshipType - )), - leftItem: observableOf(new RemoteData( - false, - false, - true, - undefined, - item, - )), - rightItem: observableOf(new RemoteData( - false, - false, - true, - undefined, - author1, - )), + relationshipType: createSuccessfulRemoteDataObject$(relationshipType), + leftItem: createSuccessfulRemoteDataObject$(item), + rightItem: createSuccessfulRemoteDataObject$(author1), }), Object.assign(new Relationship(), { self: url + '/3', id: '3', uuid: '3', - relationshipType: observableOf(new RemoteData( - false, - false, - true, - undefined, - relationshipType - )), - leftItem: observableOf(new RemoteData( - false, - false, - true, - undefined, - item, - )), - rightItem: observableOf(new RemoteData( - false, - false, - true, - undefined, - author2, - )), + relationshipType: createSuccessfulRemoteDataObject$(relationshipType), + leftItem: createSuccessfulRemoteDataObject$(item), + rightItem: createSuccessfulRemoteDataObject$(author2), }) ]; @@ -148,13 +97,7 @@ describe('EditRelationshipListComponent', () => { }, id: 'publication', uuid: 'publication', - relationships: observableOf(new RemoteData( - false, - false, - true, - undefined, - new PaginatedList(new PageInfo(), relationships), - )) + relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)) }); fieldUpdate1 = { @@ -185,8 +128,8 @@ describe('EditRelationshipListComponent', () => { relationshipService = jasmine.createSpyObj('relationshipService', { - getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))), - getItemRelationshipsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), relationships))), + getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])), + getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), isLeftItem: observableOf(true), } ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index a9434eef6f..d77a14e2f4 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -10,20 +10,15 @@ import { RelationshipIdentifiable } from '../../../../core/data/object-updates/object-updates.reducer'; import { RelationshipService } from '../../../../core/data/relationship.service'; -import {Item} from '../../../../core/shared/item.model'; -import { - defaultIfEmpty, filter, flatMap, - map, - switchMap, - take, tap, -} from 'rxjs/operators'; -import { hasValue } from '../../../../shared/empty.util'; -import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; -import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; +import { Item } from '../../../../core/shared/item.model'; +import { defaultIfEmpty, flatMap, map, switchMap, take, } 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'; import { getAllSucceededRemoteData, getRemoteDataPayload, - getSucceededRemoteData + getFirstSucceededRemoteData, } from '../../../../core/shared/operators'; import { combineLatest as observableCombineLatest, of } from 'rxjs'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; @@ -33,6 +28,8 @@ import { ItemSearchResult } from '../../../../shared/object-collection/shared/it import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { SearchResult } from '../../../../shared/search/search-result.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; @Component({ selector: 'ds-edit-relationship-list', @@ -121,10 +118,10 @@ export class EditRelationshipListComponent implements OnInit { this.relationshipType.leftType, this.relationshipType.rightType, ].map((itemTypeRD) => itemTypeRD.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ))).pipe( - map((itemTypes) => [ + map((itemTypes: ItemType[]) => [ this.relationshipType.leftwardType, this.relationshipType.rightwardType, ][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]), @@ -259,9 +256,9 @@ export class EditRelationshipListComponent implements OnInit { private getRelatedItem(relationship: Relationship): Observable { return this.relationshipService.isLeftItem(relationship, this.item).pipe( switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem), - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), - ) + ) as Observable } ngOnInit(): void { @@ -271,10 +268,11 @@ export class EditRelationshipListComponent implements OnInit { this.relationshipType.leftType, this.relationshipType.rightType, ].map((type) => type.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ))).pipe( - map((relatedTypes) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)), + map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)), + hasValueOperator() ); this.relatedEntityType$.pipe( @@ -304,9 +302,11 @@ export class EditRelationshipListComponent implements OnInit { map((fieldUpdates) => { const fieldUpdatesFiltered: FieldUpdates = {}; Object.keys(fieldUpdates).forEach((uuid) => { - const field = fieldUpdates[uuid].field; - if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) { - fieldUpdatesFiltered[uuid] = fieldUpdates[uuid]; + if (hasValue(fieldUpdates[uuid])) { + const field = fieldUpdates[uuid].field; + if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) { + fieldUpdatesFiltered[uuid] = fieldUpdates[uuid]; + } } }); return fieldUpdatesFiltered; @@ -316,26 +316,20 @@ export class EditRelationshipListComponent implements OnInit { } private getItemRelationships() { - this.linkService.resolveLink(this.item, followLink('relationships')); + this.linkService.resolveLink(this.item, + followLink('relationships', undefined, true, + followLink('relationshipType'), + followLink('leftItem'), + followLink('rightItem'), + )); return this.item.relationships.pipe( getAllSucceededRemoteData(), - map((relationships) => relationships.payload.page.filter((relationship) => relationship)), - filter((relationships) => relationships.every((relationship) => !!relationship)), - tap((relationships: Relationship[]) => - relationships.forEach((relationship: Relationship) => { - this.linkService.resolveLinks( - relationship, - followLink('relationshipType'), - followLink('leftItem'), - followLink('rightItem'), - ); - }) - ), + map((relationships: RemoteData>) => relationships.payload.page.filter((relationship: Relationship) => hasValue(relationship))), switchMap((itemRelationships: Relationship[]) => observableCombineLatest( itemRelationships .map((relationship) => relationship.relationshipType.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), )) ).pipe( diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts index 6e81319f28..667c811c07 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts @@ -4,14 +4,13 @@ import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; -import { RemoteData } from '../../../../core/data/remote-data'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { PageInfo } from '../../../../core/shared/page-info.model'; import { EditRelationshipComponent } from './edit-relationship.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; let objectUpdatesService; const url = 'http://test-url.com/test-url'; @@ -49,7 +48,7 @@ describe('EditRelationshipComponent', () => { }, id: 'publication', uuid: 'publication', - relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)) }); relatedItem = Object.assign(new Item(), { @@ -65,9 +64,9 @@ describe('EditRelationshipComponent', () => { uuid: '2', leftId: 'author1', rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)), - leftItem: observableOf(new RemoteData(false, false, true, undefined, relatedItem)), - rightItem: observableOf(new RemoteData(false, false, true, undefined, item)), + relationshipType: createSuccessfulRemoteDataObject$(relationshipType), + leftItem: createSuccessfulRemoteDataObject$(relatedItem), + rightItem: createSuccessfulRemoteDataObject$(item), }), Object.assign(new Relationship(), { _links: { @@ -77,7 +76,7 @@ describe('EditRelationshipComponent', () => { uuid: '3', leftId: 'author2', rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + relationshipType: createSuccessfulRemoteDataObject$(relationshipType) }) ]; diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index 265bca7529..502eb136fc 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -9,7 +9,7 @@ import { } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { Item } from '../../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -81,12 +81,12 @@ export class EditRelationshipComponent implements OnChanges { ngOnChanges(): void { if (this.relationship) { this.leftItem$ = this.relationship.leftItem.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) ); this.rightItem$ = this.relationship.rightItem.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 17fede115e..4e5b14ffe0 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -11,7 +11,6 @@ import { EntityTypeService } from '../../../core/data/entity-type.service'; import { ItemDataService } from '../../../core/data/item-data.service'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; import { RelationshipService } from '../../../core/data/relationship.service'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; @@ -29,6 +28,8 @@ import { NotificationsService } from '../../../shared/notifications/notification import { SharedModule } from '../../../shared/shared.module'; import { RouterStub } from '../../../shared/testing/router.stub'; import { ItemRelationshipsComponent } from './item-relationships.component'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; let comp: any; let fixture: ComponentFixture; @@ -84,7 +85,7 @@ describe('ItemRelationshipsComponent', () => { }, id: '2', uuid: '2', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + relationshipType: createSuccessfulRemoteDataObject$(relationshipType) }), Object.assign(new Relationship(), { _links: { @@ -92,7 +93,7 @@ describe('ItemRelationshipsComponent', () => { }, id: '3', uuid: '3', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + relationshipType: createSuccessfulRemoteDataObject$(relationshipType) }) ]; @@ -102,7 +103,7 @@ describe('ItemRelationshipsComponent', () => { }, id: 'publication', uuid: 'publication', - relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))), + relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), lastModified: date }); @@ -119,10 +120,10 @@ describe('ItemRelationshipsComponent', () => { uuid: 'author2' }); - relationships[0].leftItem = observableOf(new RemoteData(false, false, true, undefined, author1)); - relationships[0].rightItem = observableOf(new RemoteData(false, false, true, undefined, item)); - relationships[1].leftItem = observableOf(new RemoteData(false, false, true, undefined, author2)); - relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item)); + relationships[0].leftItem = createSuccessfulRemoteDataObject$(author1); + relationships[0].rightItem = createSuccessfulRemoteDataObject$(item); + relationships[1].leftItem = createSuccessfulRemoteDataObject$(author2); + relationships[1].rightItem = createSuccessfulRemoteDataObject$(item); fieldUpdate1 = { field: relationships[0], @@ -137,12 +138,12 @@ describe('ItemRelationshipsComponent', () => { }; itemService = jasmine.createSpyObj('itemService', { - findById: observableOf(new RemoteData(false, false, true, undefined, item)) + findById: createSuccessfulRemoteDataObject$(item) }); routeStub = { data: observableOf({}), parent: { - data: observableOf({ dso: new RemoteData(false, false, true, null, item) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } }; @@ -184,7 +185,7 @@ describe('ItemRelationshipsComponent', () => { requestService = jasmine.createSpyObj('requestService', { removeByHrefSubstring: {}, - hasByHrefObservable: observableOf(false) + hasByHref$: observableOf(false) } ); @@ -194,20 +195,8 @@ describe('ItemRelationshipsComponent', () => { entityTypeService = jasmine.createSpyObj('entityTypeService', { - getEntityTypeByLabel: observableOf(new RemoteData( - false, - false, - true, - null, - entityType, - )), - getEntityTypeRelationships: observableOf(new RemoteData( - false, - false, - true, - null, - new PaginatedList(new PageInfo(), [relationshipType]), - )), + getEntityTypeByLabel: createSuccessfulRemoteDataObject$(entityType), + getEntityTypeRelationships: createSuccessfulRemoteDataObject$(createPaginatedList([relationshipType])), } ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 25f3d1a91c..8298eebe9d 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { DeleteRelationship, @@ -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 { filter, 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'; @@ -17,16 +21,17 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RelationshipService } from '../../../core/data/relationship.service'; -import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; import { RemoteData } from '../../../core/data/remote-data'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { EntityTypeService } from '../../../core/data/entity-type.service'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-item-relationships', @@ -78,10 +83,11 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { * Update the item (and view) when it's removed in the request cache */ public initializeItemUpdate(): void { - this.itemRD$ = this.requestService.hasByHrefObservable(this.item.self).pipe( + 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')), @@ -90,7 +96,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { ); this.itemRD$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ).subscribe((item) => { this.item = item; @@ -108,7 +114,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { if (label !== undefined) { this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), ); @@ -119,7 +125,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { followLink('leftType'), followLink('rightType')) .pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((relationshipTypes) => relationshipTypes.page), ) @@ -162,6 +168,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { const addRelatedItems$: Observable = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe( map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates) + .filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate)) .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD) .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as RelationshipIdentifiable) ), @@ -191,7 +198,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { }); } - deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable { + deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable>> { return observableZip(...deleteRelationshipIDs.map((deleteRelationship) => { let copyVirtualMetadata: string; if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { @@ -208,7 +215,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { )); } - addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable { + addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable>> { return observableZip(...addRelatedItems.map((addRelationship) => this.entityType$.pipe( switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)), @@ -240,11 +247,11 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { * - Success notification in case there's at least one successful response * @param responses */ - displayNotifications(responses: RestResponse[]) { - const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); - const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful); + displayNotifications(responses: Array>) { + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - failedResponses.forEach((response: ErrorResponse) => { + failedResponses.forEach((response: RemoteData) => { this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); }); if (successfulResponses.length > 0) { diff --git a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts index 7cdd043603..5e35ea1da2 100644 --- a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts +++ b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts @@ -3,7 +3,7 @@ import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { ActivatedRoute } from '@angular/router'; import { AlertType } from '../../../shared/alert/aletr-type'; @@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent { } ngOnInit(): void { - this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable>; } } diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts index aa6a3e537a..4931fbdbd7 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts @@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Item } from '../../../core/shared/item.model'; import { RouterStub } from '../../../shared/testing/router.stub'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -15,8 +14,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ItemWithdrawComponent } from './item-withdraw.component'; import { By } from '@angular/platform-browser'; -import { RestResponse } from '../../../core/cache/response.models'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; let comp: ItemWithdrawComponent; let fixture: ComponentFixture; @@ -27,8 +25,6 @@ let routerStub; let mockItemDataService: ItemDataService; let routeStub; let notificationsServiceStub; -let successfulRestResponse; -let failRestResponse; describe('ItemWithdrawComponent', () => { beforeEach(async(() => { @@ -46,14 +42,12 @@ describe('ItemWithdrawComponent', () => { }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { - setWithDrawn: observableOf(new RestResponse(true, 200, 'OK')) + setWithDrawn: createSuccessfulRemoteDataObject$(mockItem) }); routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ - id: 'fake-id' - }) + dso: createSuccessfulRemoteDataObject(mockItem) }) }; @@ -74,9 +68,6 @@ describe('ItemWithdrawComponent', () => { })); beforeEach(() => { - successfulRestResponse = new RestResponse(true, 200, 'OK'); - failRestResponse = new RestResponse(false, 500, 'Internal Server Error'); - fixture = TestBed.createComponent(ItemWithdrawComponent); comp = fixture.componentInstance; fixture.detectChanges(); @@ -98,9 +89,8 @@ describe('ItemWithdrawComponent', () => { spyOn(comp, 'processRestResponse'); comp.performAction(); - expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, true); + expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem, true); expect(comp.processRestResponse).toHaveBeenCalled(); }); }); -}) -; +}); diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts index 1fed1756a4..13843b6d1d 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; -import { first } from 'rxjs/operators'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { RestResponse } from '../../../core/cache/response.models'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-withdraw', @@ -21,8 +20,8 @@ export class ItemWithdrawComponent extends AbstractSimpleItemActionComponent { * Perform the withdraw action to the item */ performAction() { - this.itemDataService.setWithDrawn(this.item.id, true).pipe(first()).subscribe( - (response: RestResponse) => { + this.itemDataService.setWithDrawn(this.item, true).pipe(getFirstCompletedRemoteData()).subscribe( + (response: RemoteData) => { this.processRestResponse(response); } ); diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts index 31c8a6f808..f9bc44acec 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts @@ -15,8 +15,8 @@ import { RemoteData } from '../../../core/data/remote-data'; import { AbstractSimpleItemActionComponent } from './abstract-simple-item-action.component'; import { By } from '@angular/platform-browser'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../core/cache/response.models'; import { + createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; @@ -50,8 +50,8 @@ let routerStub; let mockItemDataService; let routeStub; let notificationsServiceStub; -let successfulRestResponse; -let failRestResponse; +let successfulRemoteData; +let failedRemoteData; describe('AbstractSimpleItemActionComponent', () => { beforeEach(async(() => { @@ -97,8 +97,8 @@ describe('AbstractSimpleItemActionComponent', () => { })); beforeEach(() => { - successfulRestResponse = new RestResponse(true, 200, 'OK'); - failRestResponse = new RestResponse(false, 500, 'Internal Server Error'); + successfulRemoteData = createSuccessfulRemoteDataObject({ }); + failedRemoteData = createFailedRemoteDataObject('Internal Server Error', 500); fixture = TestBed.createComponent(MySimpleItemActionComponent); comp = fixture.componentInstance; @@ -132,15 +132,15 @@ describe('AbstractSimpleItemActionComponent', () => { expect(comp.performAction).toHaveBeenCalled(); }); - it('should process a RestResponse to navigate and display success notification', () => { - comp.processRestResponse(successfulRestResponse); + it('should process a RemoteData to navigate and display success notification', () => { + comp.processRestResponse(successfulRemoteData); expect(notificationsServiceStub.success).toHaveBeenCalled(); expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]); }); - it('should process a RestResponse to navigate and display success notification', () => { - comp.processRestResponse(failRestResponse); + it('should process a RemoteData to navigate and display success notification', () => { + comp.processRestResponse(failedRemoteData); expect(notificationsServiceStub.error).toHaveBeenCalled(); expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]); diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts index 1bd8782a30..4581863107 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts @@ -1,15 +1,14 @@ -import {Component, OnInit, Predicate} from '@angular/core'; -import {ActivatedRoute, Router} from '@angular/router'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {TranslateService} from '@ngx-translate/core'; -import {Item} from '../../../core/shared/item.model'; -import {RemoteData} from '../../../core/data/remote-data'; -import {Observable} from 'rxjs'; -import {getSucceededRemoteData} from '../../../core/shared/operators'; -import {first, map} from 'rxjs/operators'; -import {findSuccessfulAccordingTo} from '../edit-item-operators'; -import { RestResponse } from '../../../core/cache/response.models'; +import { Component, OnInit, Predicate } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Item } from '../../../core/shared/item.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Observable } from 'rxjs'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { first, map } from 'rxjs/operators'; +import { findSuccessfulAccordingTo } from '../edit-item-operators'; import { getItemEditRoute } from '../../item-page-routing-paths'; /** @@ -43,7 +42,7 @@ export class AbstractSimpleItemActionComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.data.pipe( map((data) => data.dso), - getSucceededRemoteData() + getFirstSucceededRemoteData() )as Observable>; this.itemRD$.pipe(first()).subscribe((rd) => { @@ -56,7 +55,6 @@ export class AbstractSimpleItemActionComponent implements OnInit { this.headerMessage = 'item.edit.' + this.messageKey + '.header'; this.descriptionMessage = 'item.edit.' + this.messageKey + '.description'; } - /** * Perform the operation linked to this action */ @@ -68,8 +66,8 @@ export class AbstractSimpleItemActionComponent implements OnInit { * Process the response obtained during the performAction method and navigate back to the edit page * @param response from the action in the performAction method */ - processRestResponse(response: RestResponse) { - if (response.isSuccessful) { + processRestResponse(response: RemoteData) { + if (response.hasSucceeded) { this.itemDataService.findById(this.item.id).pipe( findSuccessfulAccordingTo(this.predicate)).subscribe(() => { this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success')); diff --git a/src/app/+item-page/field-components/collections/collections.component.spec.ts b/src/app/+item-page/field-components/collections/collections.component.spec.ts index 8e5ca6dd3c..1ff9e497e8 100644 --- a/src/app/+item-page/field-components/collections/collections.component.spec.ts +++ b/src/app/+item-page/field-components/collections/collections.component.spec.ts @@ -7,7 +7,10 @@ import { CollectionDataService } from '../../../core/data/collection-data.servic import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; import { CollectionsComponent } from './collections.component'; let collectionsComponent: CollectionsComponent; @@ -23,11 +26,14 @@ const mockCollection1: Collection = Object.assign(new Collection(), { value: 'Short description' } ] + }, + _links: { + self: { href: 'collection-selflink' } } }); const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: createSuccessfulRemoteDataObject$(mockCollection1)}); -const failedMockItem: Item = Object.assign(new Item(), {owningCollection: createFailedRemoteDataObject$(mockCollection1)}); +const failedMockItem: Item = Object.assign(new Item(), {owningCollection: createFailedRemoteDataObject$('error', 500)}); describe('CollectionsComponent', () => { collectionDataServiceStub = { @@ -35,7 +41,7 @@ describe('CollectionsComponent', () => { if (item === succeededMockItem) { return createSuccessfulRemoteDataObject$(mockCollection1); } else { - return createFailedRemoteDataObject$(mockCollection1); + return createFailedRemoteDataObject$('error', 500); } } }; diff --git a/src/app/+item-page/field-components/collections/collections.component.ts b/src/app/+item-page/field-components/collections/collections.component.ts index 0d50fcad83..32dc8dfb73 100644 --- a/src/app/+item-page/field-components/collections/collections.component.ts +++ b/src/app/+item-page/field-components/collections/collections.component.ts @@ -2,12 +2,13 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { PageInfo } from '../../../core/shared/page-info.model'; +import { hasValue } from '../../../shared/empty.util'; /** * This component renders the parent collections section of the item @@ -40,18 +41,23 @@ export class CollectionsComponent implements OnInit { // only the owning collection this.collectionsRD$ = this.cds.findOwningCollectionFor(this.item).pipe( map((rd: RemoteData) => { - if (rd.hasSucceeded) { + if (hasValue(rd.payload)) { return new RemoteData( - false, - false, - true, - undefined, - new PaginatedList({ + rd.timeCompleted, + rd.msToLive, + rd.lastUpdated, + rd.state, + rd.errorMessage, + buildPaginatedList({ elementsPerPage: 10, totalPages: 1, currentPage: 1, - totalElements: 1 - } as PageInfo, [rd.payload]) + totalElements: 1, + _links: { + self: rd.payload._links.self + } + } as PageInfo, [rd.payload]), + rd.statusCode ); } else { return rd as any; diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index bd3b2f7063..67c0ca8fa9 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -7,7 +7,7 @@ import { Item } from '../../../../core/shared/item.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { switchMap } from 'rxjs/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; @@ -67,11 +67,12 @@ export class FullFileSectionComponent extends FileSectionComponent implements On this.item, 'ORIGINAL', {elementsPerPage: this.pageSize, currentPage: pageNumber}, + true, followLink('format') )), tap((rd: RemoteData>) => { - if (hasValue(rd.error)) { - this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.error.statusCode} ${rd.error.message}`); + if (hasValue(rd.errorMessage)) { + this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`); } } ) @@ -82,11 +83,12 @@ export class FullFileSectionComponent extends FileSectionComponent implements On this.item, 'LICENSE', {elementsPerPage: this.pageSize, currentPage: pageNumber}, + true, followLink('format') )), tap((rd: RemoteData>) => { - if (hasValue(rd.error)) { - this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.error.statusCode} ${rd.error.message}`); + if (hasValue(rd.errorMessage)) { + this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`); } } ) diff --git a/src/app/+item-page/full/full-item-page.component.spec.ts b/src/app/+item-page/full/full-item-page.component.spec.ts index 85a2c897ad..24123ebca3 100644 --- a/src/app/+item-page/full/full-item-page.component.spec.ts +++ b/src/app/+item-page/full/full-item-page.component.spec.ts @@ -11,8 +11,6 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { PaginatedList } from '../../core/data/paginated-list'; import { of as observableOf } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; @@ -21,9 +19,10 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { AuthService } from '../../core/auth/auth.service'; +import { createPaginatedList } from '../../shared/testing/utils.test'; const mockItem: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: { 'dc.title': [ { diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 63a560778b..1b5eb05128 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -4,10 +4,9 @@ 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 { hasValue } from '../shared/empty.util'; -import { find } from 'rxjs/operators'; import { followLink } from '../shared/utils/follow-link-config.model'; import { FindListOptions } from '../core/data/request.models'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * This class represents a resolver that requests a specific item before the route is activated @@ -26,12 +25,13 @@ export class ItemPageResolver implements Resolve> { */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.itemService.findById(route.params.id, + false, followLink('owningCollection'), followLink('bundles', new FindListOptions(), true, followLink('bitstreams')), followLink('relationships'), followLink('version', undefined, true, followLink('versionhistory')), ).pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + getFirstCompletedRemoteData(), ); } } diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts index 4b60691e09..d28b579996 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts @@ -4,12 +4,12 @@ import { BitstreamDataService } from '../../../../core/data/bitstream-data.servi import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { filter, take } from 'rxjs/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { hasValue } from '../../../../shared/empty.util'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; /** * This component renders the file section of the item @@ -66,11 +66,10 @@ export class FileSectionComponent implements OnInit { currentPage: this.currentPage, elementsPerPage: this.pageSize }).pipe( - filter((bitstreamsRD: RemoteData>) => hasValue(bitstreamsRD) && (hasValue(bitstreamsRD.error) || hasValue(bitstreamsRD.payload))), - take(1), + getFirstCompletedRemoteData(), ).subscribe((bitstreamsRD: RemoteData>) => { - if (bitstreamsRD.error) { - this.notificationsService.error(this.translateService.get('file-section.error.header'), `${bitstreamsRD.error.statusCode} ${bitstreamsRD.error.message}`); + if (bitstreamsRD.errorMessage) { + this.notificationsService.error(this.translateService.get('file-section.error.header'), `${bitstreamsRD.statusCode} ${bitstreamsRD.errorMessage}`); } else if (hasValue(bitstreamsRD.payload)) { const current: Bitstream[] = this.bitstreams$.getValue(); this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]); diff --git a/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index 4459408644..637f9f9593 100644 --- a/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -2,13 +2,12 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Item } from '../../../../core/shared/item.model'; -import { PaginatedList } from '../../../../core/data/paginated-list'; import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; -import { PageInfo } from '../../../../core/shared/page-info.model'; import { ItemPageFieldComponent } from './item-page-field.component'; import { MetadataValuesComponent } from '../../../field-components/metadata-values/metadata-values.component'; import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; let comp: ItemPageFieldComponent; let fixture: ComponentFixture; @@ -50,7 +49,7 @@ describe('ItemPageFieldComponent', () => { export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item { const item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: new MetadataMap() }); item.metadata[field] = [{ diff --git a/src/app/+item-page/simple/item-page.component.spec.ts b/src/app/+item-page/simple/item-page.component.spec.ts index bdd9d35641..837eec879a 100644 --- a/src/app/+item-page/simple/item-page.component.spec.ts +++ b/src/app/+item-page/simple/item-page.component.spec.ts @@ -9,20 +9,21 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { MetadataService } from '../../core/metadata/metadata.service'; import { VarDirective } from '../../shared/utils/var.directive'; import { Item } from '../../core/shared/item.model'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createRelationshipsObservable } from './item-types/shared/item.component.spec'; import { of as observableOf } from 'rxjs'; import { - createFailedRemoteDataObject$, createPendingRemoteDataObject$, createSuccessfulRemoteDataObject, + createFailedRemoteDataObject$, + createPendingRemoteDataObject$, + createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { AuthService } from '../../core/auth/auth.service'; +import { createPaginatedList } from '../../shared/testing/utils.test'; const mockItem: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], relationships: createRelationshipsObservable() }); @@ -77,7 +78,7 @@ describe('ItemPageComponent', () => { describe('when the item is loading', () => { beforeEach(() => { - comp.itemRD$ = createPendingRemoteDataObject$(undefined); + comp.itemRD$ = createPendingRemoteDataObject$(); // comp.itemRD$ = observableOf(new RemoteData(true, true, true, null, undefined)); fixture.detectChanges(); }); @@ -90,7 +91,7 @@ describe('ItemPageComponent', () => { describe('when the item failed loading', () => { beforeEach(() => { - comp.itemRD$ = createFailedRemoteDataObject$(undefined); + comp.itemRD$ = createFailedRemoteDataObject$('server error', 500); fixture.detectChanges(); }); diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/+item-page/simple/item-types/publication/publication.component.spec.ts index 1d340e2e73..2f16d199ed 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/+item-page/simple/item-types/publication/publication.component.spec.ts @@ -12,14 +12,12 @@ import { CommunityDataService } from '../../../../core/data/community-data.servi import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { Item } from '../../../../core/shared/item.model'; import { MetadataMap } from '../../../../core/shared/metadata.models'; -import { PageInfo } from '../../../../core/shared/page-info.model'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; @@ -29,9 +27,10 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { createRelationshipsObservable } from '../shared/item.component.spec'; import { PublicationComponent } from './publication.component'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; const mockItem: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: new MetadataMap(), relationships: createRelationshipsObservable() }); diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts index 6ef035f1e6..2772dffeba 100644 --- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts @@ -1,11 +1,14 @@ import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteData +} from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; /** @@ -72,7 +75,7 @@ export const relationsToItems = (thisId: string) => export const paginatedRelationsToItems = (thisId: string) => (source: Observable>>): Observable>> => source.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), switchMap((relationshipsRD: RemoteData>) => { return observableCombineLatest( relationshipsRD.payload.page.map((rel: Relationship) => diff --git a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts index fe33dfe237..5bf2fb3aca 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts @@ -12,7 +12,6 @@ import { CommunityDataService } from '../../../../core/data/community-data.servi import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { Bitstream } from '../../../../core/shared/bitstream.model'; @@ -20,7 +19,6 @@ import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { PageInfo } from '../../../../core/shared/page-info.model'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { isNotEmpty } from '../../../../shared/empty.util'; import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; @@ -31,6 +29,7 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils'; import { ItemComponent } from './item.component'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; /** * Create a generic test for an item-page-fields component using a mockItem and the type of component @@ -117,7 +116,7 @@ export function containsFieldInput(fields: DebugElement[], metadataKey: string): } export function createRelationshipsObservable() { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [ + return createSuccessfulRemoteDataObject$(createPaginatedList([ Object.assign(new Relationship(), { relationshipType: createSuccessfulRemoteDataObject$(new RelationshipType()), leftItem: createSuccessfulRemoteDataObject$(new Item()), diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index 83585dead5..93a2a22365 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -12,7 +12,7 @@ import { CommunityDataService } from '../../../../core/data/community-data.servi import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { Bitstream } from '../../../../core/shared/bitstream.model'; @@ -31,7 +31,7 @@ import { createRelationshipsObservable } from '../shared/item.component.spec'; import { UntypedItemComponent } from './untyped-item.component'; const mockItem: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: new MetadataMap(), relationships: createRelationshipsObservable() }); diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index fc696482d0..3b79b6f1d8 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -3,7 +3,7 @@ import { MetadataRepresentation } from '../../../core/shared/metadata-representa 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 { getSucceededRemoteData } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { filter, map, switchMap } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; @@ -82,8 +82,8 @@ 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, followLink('leftItem'), followLink('rightItem')).pipe( - getSucceededRemoteData(), + return this.relationshipService.findById(metadatum.virtualValue, false, followLink('leftItem'), followLink('rightItem')).pipe( + getFirstSucceededRemoteData(), switchMap((relRD: RemoteData) => observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded), diff --git a/src/app/+item-page/simple/related-items/related-items-component.ts b/src/app/+item-page/simple/related-items/related-items-component.ts index 0446e53be5..5029809e0b 100644 --- a/src/app/+item-page/simple/related-items/related-items-component.ts +++ b/src/app/+item-page/simple/related-items/related-items-component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; import { FindListOptions } from '../../../core/data/request.models'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { RelationshipService } from '../../../core/data/relationship.service'; diff --git a/src/app/+item-page/simple/related-items/related-items.component.spec.ts b/src/app/+item-page/simple/related-items/related-items.component.spec.ts index cbfaa3b562..1a40c74340 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.spec.ts +++ b/src/app/+item-page/simple/related-items/related-items.component.spec.ts @@ -2,8 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { RelatedItemsComponent } from './related-items-component'; import { Item } from '../../../core/shared/item.model'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { PageInfo } from '../../../core/shared/page-info.model'; import { By } from '@angular/platform-browser'; import { createRelationshipsObservable } from '../item-types/shared/item.component.spec'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; @@ -11,19 +9,20 @@ import { RelationshipService } from '../../../core/data/relationship.service'; import { TranslateModule } from '@ngx-translate/core'; import { VarDirective } from '../../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; const parentItem: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], relationships: createRelationshipsObservable() }); const mockItem1: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], relationships: createRelationshipsObservable() }); const mockItem2: Item = Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], relationships: createRelationshipsObservable() }); @@ -38,7 +37,7 @@ describe('RelatedItemsComponent', () => { beforeEach(async(() => { relationshipService = jasmine.createSpyObj('relationshipService', { - getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockItems)), + getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(mockItems)), } ); diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts index 982d06aa75..8d71f2e397 100644 --- a/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts @@ -1,5 +1,4 @@ import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; - import { CollectionSelectorComponent } from './collection-selector.component'; import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component'; import { Collection } from 'src/app/core/shared/collection.model'; @@ -8,9 +7,8 @@ import { RemoteData } from 'src/app/core/data/remote-data'; import { Community } from 'src/app/core/shared/community.model'; import { FindListOptions } from 'src/app/core/data/request.models'; import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; -import { PaginatedList } from 'src/app/core/data/paginated-list'; -import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; -import { PageInfo } from 'src/app/core/shared/page-info.model'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock'; import { CollectionDataService } from 'src/app/core/data/collection-data.service'; @@ -19,6 +17,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ActivatedRoute } from '@angular/router'; import { hot } from 'jasmine-marbles'; import { By } from '@angular/platform-browser'; +import { createPaginatedList } from '../../shared/testing/utils.test'; describe('CollectionSelectorComponent', () => { let component: CollectionSelectorComponent; @@ -41,9 +40,7 @@ describe('CollectionSelectorComponent', () => { language: 'en_US', value: 'Community 1-Collection 1' }], - parentCommunity: of( - new RemoteData(false, false, true, undefined, community, 200) - ) + parentCommunity: createSuccessfulRemoteDataObject$(community) }), Object.assign(new Collection(), { id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', @@ -54,9 +51,7 @@ describe('CollectionSelectorComponent', () => { language: 'en_US', value: 'Community 1-Collection 2' }], - parentCommunity: of( - new RemoteData(false, false, true, undefined, community, 200) - ) + parentCommunity: createSuccessfulRemoteDataObject$(community) }), Object.assign(new Collection(), { id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', @@ -67,9 +62,7 @@ describe('CollectionSelectorComponent', () => { language: 'en_US', value: 'Community 1-Collection 3' }], - parentCommunity: of( - new RemoteData(false, false, true, undefined, community, 200) - ) + parentCommunity: createSuccessfulRemoteDataObject$(community) }), Object.assign(new Collection(), { id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', @@ -80,9 +73,7 @@ describe('CollectionSelectorComponent', () => { language: 'en_US', value: 'Community 1-Collection 4' }], - parentCommunity: of( - new RemoteData(false, false, true, undefined, community, 200) - ) + parentCommunity: createSuccessfulRemoteDataObject$(community) }), Object.assign(new Collection(), { id: 'a5159760-f362-4659-9e81-e3253ad91ede', @@ -93,9 +84,7 @@ describe('CollectionSelectorComponent', () => { language: 'en_US', value: 'Community 1-Collection 5' }], - parentCommunity: of( - new RemoteData(false, false, true, undefined, community, 200) - ) + parentCommunity: createSuccessfulRemoteDataObject$(community) }) ]; @@ -103,9 +92,7 @@ describe('CollectionSelectorComponent', () => { const collectionDataServiceMock = { getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { return hot( 'a|', { - a: createSuccessfulRemoteDataObject( - new PaginatedList(new PageInfo(), collections) - ) + a: createSuccessfulRemoteDataObject(createPaginatedList(collections)) }); } }; diff --git a/src/app/+my-dspace-page/my-dspace-page.component.ts b/src/app/+my-dspace-page/my-dspace-page.component.ts index 6556eaaf1f..3e58bb8e0f 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -10,7 +10,7 @@ import { import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { map, switchMap, tap, } from 'rxjs/operators'; -import { PaginatedList } from '../core/data/paginated-list'; +import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { pushInOut } from '../shared/animations/push'; @@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option import { SearchService } from '../core/shared/search/search.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { hasValue } from '../shared/empty.util'; -import { getSucceededRemoteData } from '../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { RoleType } from '../core/roles/role-types'; @@ -126,7 +126,7 @@ export class MyDSpacePageComponent implements OnInit { this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.sub = this.searchOptions$.pipe( tap(() => this.resultsRD$.next(null)), - switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData()))) + switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData()))) .subscribe((results) => { this.resultsRD$.next(results); }); diff --git a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html index 63eb0ff9a7..3a829e6ece 100644 --- a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html +++ b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html @@ -9,5 +9,5 @@ - +

{{'mydspace.results.no-results' | translate}}

diff --git a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts index abf110d356..35b13c8bae 100644 --- a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts +++ b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts @@ -2,8 +2,7 @@ import { Component, Input } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; -import { SearchOptions } from '../../shared/search/search-options.model'; -import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { ViewMode } from '../../core/shared/view-mode.model'; import { isEmpty } from '../../shared/empty.util'; import { Context } from '../../core/shared/context.model'; diff --git a/src/app/+search-page/search-tracker.component.ts b/src/app/+search-page/search-tracker.component.ts index 7e5aa49165..4161d6b19f 100644 --- a/src/app/+search-page/search-tracker.component.ts +++ b/src/app/+search-page/search-tracker.component.ts @@ -1,18 +1,19 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; -import { filter, map, switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { SearchComponent } from './search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { HostWindowService } from '../shared/host-window.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { RouteService } from '../core/services/route.service'; -import { hasValue } from '../shared/empty.util'; -import { SearchSuccessResponse } from '../core/cache/response.models'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchService } from '../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; -import { SearchQueryResponse } from '../shared/search/search-query-response.model'; +import { SearchObjects } from '../shared/search/search-objects.model'; import { Router } from '@angular/router'; +import { RemoteData } from '../core/data/remote-data'; +import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; /** * This component triggers a page view statistic @@ -45,23 +46,15 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { ngOnInit(): void { // super.ngOnInit(); this.getSearchOptions().pipe( - switchMap((options) => this.service.searchEntries(options) - .pipe( - filter((entry) => - hasValue(entry.requestEntry) - && hasValue(entry.requestEntry.response) - && entry.requestEntry.response.isSuccessful === true - ), - map((entry) => ({ - searchOptions: entry.searchOptions, - response: (entry.requestEntry.response as SearchSuccessResponse).results - })), - ) - ) - ) - .subscribe((entry) => { - const config: PaginatedSearchOptions = entry.searchOptions; - const searchQueryResponse: SearchQueryResponse = entry.response; + switchMap((options: PaginatedSearchOptions) => + this.service.searchEntries(options).pipe( + getFirstSucceededRemoteData(), + map((rd: RemoteData>) => ({ + config: options, + searchQueryResponse: rd.payload + })) + )), + ).subscribe(({ config, searchQueryResponse }) => { const filters: Array<{ filter: string, operator: string, value: string, label: string; }> = []; const appliedFilters = searchQueryResponse.appliedFilters || []; for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { @@ -74,8 +67,8 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { searchOptions: config, page: { size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage - totalElements: searchQueryResponse.page.totalElements, - totalPages: searchQueryResponse.page.totalPages, + totalElements: searchQueryResponse.pageInfo.totalElements, + totalPages: searchQueryResponse.pageInfo.totalPages, number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage }, sort: { diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index bbbfdba513..c3d416fb27 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { startWith, switchMap, } from 'rxjs/operators'; -import { PaginatedList } from '../core/data/paginated-list'; +import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { pushInOut } from '../shared/animations/push'; import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { hasValue, isNotEmpty } from '../shared/empty.util'; -import { getSucceededRemoteData } from '../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { RouteService } from '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; @@ -122,7 +122,7 @@ export class SearchComponent implements OnInit { this.searchLink = this.getSearchLink(); this.searchOptions$ = this.getSearchOptions(); this.sub = this.searchOptions$.pipe( - switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined)))) + switchMap((options) => this.service.search(options).pipe(getFirstSucceededRemoteData(), startWith(undefined)))) .subscribe((results) => { this.resultsRD$.next(results); }); diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 72a39ab53c..451469b5e4 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -8,7 +8,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { TranslateService } from '@ngx-translate/core'; import { RequestService } from '../../core/data/request.service'; import { map } from 'rxjs/operators'; -import { RestResponse } from '../../core/cache/response.models'; +import { RemoteData } from '../../core/data/remote-data'; +import { NoContent } from '../../core/shared/NoContent.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @Component({ selector: 'ds-workflow-item-delete', @@ -41,6 +43,9 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent */ sendRequest(id: string): Observable { this.requestService.removeByHrefSubstring('/discover'); - return this.workflowItemService.delete(id).pipe(map((response: RestResponse) => response.isSuccessful)); + return this.workflowItemService.delete(id).pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => response.hasSucceeded) + ); } } diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts index 792c642ec7..8f6a1f1de0 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -1,7 +1,7 @@ import { first } from 'rxjs/operators'; -import { of as observableOf } from 'rxjs'; import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; describe('WorkflowItemPageResolver', () => { describe('resolve', () => { @@ -11,17 +11,18 @@ describe('WorkflowItemPageResolver', () => { beforeEach(() => { wfiService = { - findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) as any + findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) } as any; resolver = new WorkflowItemPageResolver(wfiService); }); - it('should resolve a workflow item with the correct id', () => { + it('should resolve a workflow item with the correct id', (done) => { resolver.resolve({ params: { id: uuid } } as any, undefined) .pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); + done(); } ); }); diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts index 19cc4b4914..6fc960f13c 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts @@ -2,11 +2,10 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; -import { hasValue } from '../shared/empty.util'; -import { find } from 'rxjs/operators'; import { followLink } from '../shared/utils/follow-link-config.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * This class represents a resolver that requests a specific workflow item before the route is activated @@ -25,9 +24,10 @@ export class WorkflowItemPageResolver implements Resolve> { return this.workflowItemService.findById(route.params.id, + false, followLink('item'), ).pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), + getFirstCompletedRemoteData(), ); } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 00a0e94054..ddf4ab2e68 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -18,6 +18,7 @@ import { import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths'; import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths'; +import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths'; import { ReloadGuard } from './core/reload/reload.guard'; import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard'; import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; @@ -66,7 +67,7 @@ import { ForbiddenComponent } from './forbidden/forbidden.component'; path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, - { path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, + { path: PROCESS_MODULE_PATH, loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, { path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' }, { path: FORBIDDEN_PATH, component: ForbiddenComponent }, { diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index 6868b8d546..2f3e70baac 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -4,7 +4,7 @@ import { of as observableOf } from 'rxjs'; import { take } from 'rxjs/operators'; import { AppState } from '../app.reducer'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { PaginatedList } from '../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../core/data/paginated-list.model'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { StoreMock } from '../shared/testing/store.mock'; import { CommunityListService, FlatNode, toFlatNode } from './community-list-service'; @@ -68,28 +68,28 @@ describe('CommunityListService', () => { Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), }), Object.assign(new Community(), { id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), }), Object.assign(new Community(), { id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), }), ]; mockListOfTopCommunitiesPage2 = [ Object.assign(new Community(), { id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), }), ]; mockTopCommunitiesWithChildrenArraysPage1 = [ @@ -137,7 +137,7 @@ describe('CommunityListService', () => { if (endPageIndex > allTopComs.length) { endPageIndex = allTopComs.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allTopComs.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allTopComs.slice(startPageIndex, endPageIndex))); }, findByParent(parentUUID: string, options: FindListOptions = {}) { const foundCom = allCommunities.find((community) => (community.id === parentUUID)); @@ -147,7 +147,7 @@ describe('CommunityListService', () => { currentPage = 1 } if (elementsPerPage === 0) { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), (foundCom.subcommunities as [Community]))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), (foundCom.subcommunities as [Community]))); } elementsPerPage = standardElementsPerPage; if (foundCom !== undefined && foundCom.subcommunities !== undefined) { @@ -157,7 +157,7 @@ describe('CommunityListService', () => { if (endPageIndex > coms.length) { endPageIndex = coms.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), coms.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), coms.slice(startPageIndex, endPageIndex))); } else { return createFailedRemoteDataObject$(); } @@ -172,7 +172,7 @@ describe('CommunityListService', () => { currentPage = 1 } if (elementsPerPage === 0) { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), (foundCom.collections as [Collection]))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), (foundCom.collections as [Collection]))); } elementsPerPage = standardElementsPerPage; if (foundCom !== undefined && foundCom.collections !== undefined) { @@ -182,7 +182,7 @@ describe('CommunityListService', () => { if (endPageIndex > colls.length) { endPageIndex = colls.length; } - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), colls.slice(startPageIndex, endPageIndex))); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), colls.slice(startPageIndex, endPageIndex))); } else { return createFailedRemoteDataObject$(); } @@ -361,7 +361,7 @@ describe('CommunityListService', () => { let flatNodeList; describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => { beforeEach((done) => { - service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null) + service.transformListOfCommunities(buildPaginatedList(new PageInfo(), listOfCommunities), 0, null, null) .pipe(take(1)) .subscribe((value) => { flatNodeList = value; @@ -391,7 +391,7 @@ describe('CommunityListService', () => { communityFlatNode.currentCommunityPage = 1; expandedNodes.push(communityFlatNode); }); - service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes) + service.transformListOfCommunities(buildPaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes) .pipe(take(1)) .subscribe((value) => { flatNodeList = value; @@ -421,8 +421,8 @@ describe('CommunityListService', () => { const communityWithNoSubcomsOrColls = Object.assign(new Community(), { id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 2' }] @@ -453,8 +453,8 @@ describe('CommunityListService', () => { const communityWithSubcoms = Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 1' }] @@ -487,8 +487,8 @@ describe('CommunityListService', () => { const communityWithSubcoms = Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 1' }] @@ -532,8 +532,8 @@ describe('CommunityListService', () => { communityWithCollections = Object.assign(new Community(), { id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 1' }] @@ -583,8 +583,8 @@ describe('CommunityListService', () => { const communityWithSubcoms = Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 1' }] @@ -599,8 +599,8 @@ describe('CommunityListService', () => { const communityWithCollections = Object.assign(new Community(), { id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockCollectionsPage1)), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockCollectionsPage1)), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 2' }] @@ -617,8 +617,8 @@ describe('CommunityListService', () => { const communityWithNoSubcomsOrColls = Object.assign(new Community(), { id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, no coll' }], 'dc.title': [{ language: 'en_US', value: 'Community 3' }] diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 5905fd8639..95ca34f31b 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { createSelector, Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; import { Observable, of as observableOf } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, filter } from 'rxjs/operators'; import { AppState } from '../app.reducer'; import { CommunityDataService } from '../core/data/community-data.service'; import { FindListOptions } from '../core/data/request.models'; @@ -11,12 +11,13 @@ import { Collection } from '../core/shared/collection.model'; import { PageInfo } from '../core/shared/page-info.model'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { RemoteData } from '../core/data/remote-data'; -import { PaginatedList } from '../core/data/paginated-list'; +import { PaginatedList, buildPaginatedList } from '../core/data/paginated-list.model'; import { CollectionDataService } from '../core/data/collection-data.service'; import { CommunityListSaveAction } from './community-list.actions'; import { CommunityListState } from './community-list.reducer'; import { getCommunityPageRoute } from '../+community-page/community-page-routing-paths'; import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths'; +import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators'; /** * Each node in the tree is represented by a flatNode which contains info about the node itself and its position and @@ -46,7 +47,8 @@ export class ShowMoreFlatNode { // Helper method to combine an flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Array>): Observable => observableCombineLatest(...obsList).pipe( - map((matrix: any[][]) => [].concat(...matrix)) + map((matrix: any[][]) => [].concat(...matrix)), + filter((arr: any[]) => arr.every((e) => hasValue(e))), ); /** @@ -144,10 +146,13 @@ export class CommunityListService { if (coms && coms.length > 0) { newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage }) } - return new PaginatedList(newPageInfo, newPage); + return buildPaginatedList(newPageInfo, newPage); }) ); - return topComs$.pipe(switchMap((topComs: PaginatedList) => this.transformListOfCommunities(topComs, 0, null, expandedNodes))); + return topComs$.pipe( + switchMap((topComs: PaginatedList) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)), + // distinctUntilChanged((a: FlatNode[], b: FlatNode[]) => a.length === b.length) + ); }; /** @@ -162,6 +167,7 @@ export class CommunityListService { direction: options.sort.direction } }).pipe( + getFirstSucceededRemoteData(), map((results) => results.payload), ); } @@ -227,11 +233,12 @@ export class CommunityListService { currentPage: i }) .pipe( + getFirstCompletedRemoteData(), switchMap((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { return this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes); } else { - return []; + return observableOf([]); } }) ); @@ -249,6 +256,7 @@ export class CommunityListService { currentPage: i }) .pipe( + getFirstCompletedRemoteData(), map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { let nodes = rd.payload.page diff --git a/src/app/community-list-page/community-list.reducer.spec.ts b/src/app/community-list-page/community-list.reducer.spec.ts index 23999e6166..65065185f9 100644 --- a/src/app/community-list-page/community-list.reducer.spec.ts +++ b/src/app/community-list-page/community-list.reducer.spec.ts @@ -1,5 +1,5 @@ import { of as observableOf } from 'rxjs/internal/observable/of'; -import { PaginatedList } from '../core/data/paginated-list'; +import { buildPaginatedList } from '../core/data/paginated-list.model'; import { Community } from '../core/shared/community.model'; import { PageInfo } from '../core/shared/page-info.model'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; @@ -17,8 +17,8 @@ describe('communityListReducer', () => { Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community1', }), observableOf(true), 0, false, null ); diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index 07e06f3131..ab28baaa36 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -1,5 +1,4 @@ - - + { Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community1', }), observableOf(true), 0, false, null ), @@ -95,8 +95,8 @@ describe('CommunityListComponent', () => { Object.assign(new Community(), { id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), name: 'community2', }), observableOf(true), 0, false, null ), @@ -104,8 +104,8 @@ describe('CommunityListComponent', () => { Object.assign(new Community(), { id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community3', }), observableOf(false), 0, false, null ), diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 93f55389f9..18111340f4 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,21 +1,21 @@ -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; +import { Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { isNotEmpty } from '../../shared/empty.util'; import { - AuthGetRequest, - AuthPostRequest, GetRequest, PostRequest, RestRequest, - TokenPostRequest } from '../data/request.models'; -import { AuthStatusResponse, ErrorResponse, TokenResponse } from '../cache/response.models'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { getResponseFromEntry } from '../shared/operators'; -import { HttpClient } from '@angular/common/http'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RemoteData } from '../data/remote-data'; +import { AuthStatus } from './models/auth-status.model'; +import { ShortLivedToken } from './models/short-lived-token.model'; +import { URLCombiner } from '../url-combiner/url-combiner'; @Injectable() export class AuthRequestService { @@ -25,19 +25,13 @@ export class AuthRequestService { constructor(protected halService: HALEndpointService, protected requestService: RequestService, - private http: HttpClient) { + private rdbService: RemoteDataBuildService + ) { } - protected fetchRequest(request: RestRequest): Observable { - return this.requestService.getByUUID(request.uuid).pipe( - getResponseFromEntry(), - mergeMap((response) => { - if (response.isSuccessful && isNotEmpty(response)) { - return observableOf((response as AuthStatusResponse).response); - } else if (!response.isSuccessful) { - return observableThrowError(new Error((response as ErrorResponse).errorMessage)); - } - }) + protected fetchRequest(request: RestRequest): Observable> { + return this.rdbService.buildFromRequestUUID(request.uuid).pipe( + getFirstCompletedRemoteData(), ); } @@ -45,12 +39,12 @@ export class AuthRequestService { return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; } - public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable { + public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable> { return this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), - map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), + map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), map ((request: PostRequest) => { request.responseMsToLive = 10 * 1000; return request; @@ -60,14 +54,14 @@ export class AuthRequestService { distinctUntilChanged()); } - public getRequest(method: string, options?: HttpOptions): Observable { + public getRequest(method: string, options?: HttpOptions): Observable> { return this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), - map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)), + map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), map ((request: GetRequest) => { - request.responseMsToLive = 10 * 1000; + request.forceBypassCache = true; return request; }), tap((request: GetRequest) => this.requestService.configure(request)), @@ -79,14 +73,21 @@ export class AuthRequestService { * Send a POST request to retrieve a short-lived token which provides download access of restricted files */ public getShortlivedToken(): Observable { - return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe( + return this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), distinctUntilChanged(), - map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)), + 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)), - switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)), - getResponseFromEntry(), - map((response: TokenResponse) => response.token) + switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + if (response.hasSucceeded) { + return response.payload.value; + } else { + return null; + } + }) ); } } diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts deleted file mode 100644 index 924c60535d..0000000000 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { async, TestBed } from '@angular/core/testing'; - -import { Store, StoreModule } from '@ngrx/store'; - -import { GlobalConfig } from '../../../config/global-config.interface'; -import { StoreMock } from '../../shared/testing/store.mock'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { AuthStatusResponse } from '../cache/response.models'; -import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; -import { AuthResponseParsingService } from './auth-response-parsing.service'; -import { AuthStatus } from './models/auth-status.model'; -import { storeModuleConfig } from '../../app.reducer'; - -describe('AuthResponseParsingService', () => { - let service: AuthResponseParsingService; - let linkServiceStub: any; - - const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any; - let store: any; - let objectCacheService: ObjectCacheService; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({}, storeModuleConfig), - ], - providers: [ - { provide: Store, useClass: StoreMock } - ] - }).compileComponents(); - })); - - beforeEach(() => { - store = TestBed.get(Store); - linkServiceStub = jasmine.createSpyObj({ - removeResolvedLinks: {} - }); - objectCacheService = new ObjectCacheService(store as any, linkServiceStub); - service = new AuthResponseParsingService(objectCacheService); - }); - - describe('parse', () => { - let validRequest; - let validRequest2; - let validResponse; - let validResponse1; - let validResponse2; - beforeEach(() => { - - validRequest = new AuthPostRequest( - '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', - 'https://rest.api/dspace-spring-rest/api/authn/login', - 'password=test&user=myself@testshib.org'); - - validRequest2 = new AuthGetRequest( - '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', - 'https://rest.api/dspace-spring-rest/api/authn/status'); - - validResponse = { - payload: { - authenticated: true, - id: null, - okay: true, - token: { - accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiI0ZGM3MGFiNS1jZDczLTQ5MmYtYjAwNy0zMTc5ZDJkOTI5NmIiLCJzZyI6W10sImV4cCI6MTUyNjMxODMyMn0.ASmvcbJFBfzhN7D5ncloWnaVZr5dLtgTuOgHaCKiimc', - expires: 1526318322000 - }, - } as AuthStatus, - statusCode: 200, - statusText: '200' - }; - - validResponse1 = { - payload: {}, - statusCode: 404, - statusText: '404' - }; - - validResponse2 = { - payload: { - authenticated: true, - id: null, - okay: true, - type: 'status', - _embedded: { - eperson: { - canLogIn: true, - email: 'myself@testshib.org', - groups: [], - handle: null, - id: '4dc70ab5-cd73-492f-b007-3179d2d9296b', - lastActive: '2018-05-14T17:03:31.277+0000', - metadata: { - 'eperson.firstname': [ - { - language: null, - value: 'User' - } - ], - 'eperson.lastname': [ - { - language: null, - value: 'Test' - } - ], - 'eperson.language': [ - { - language: null, - value: 'en' - } - ] - }, - name: 'User Test', - netid: 'myself@testshib.org', - requireCertificate: false, - selfRegistered: false, - type: 'eperson', - uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', - _links: { - self: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' - } - } - } - }, - _links: { - eperson: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' - }, - self: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' - } - } - }, - statusCode: 200, - statusText: '200' - - }; - }); - - it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => { - const response = service.parse(validRequest, validResponse); - expect(response.constructor).toBe(AuthStatusResponse); - }); - - it('should return a AuthStatusResponse if data contains a valid endpoint response', () => { - const response = service.parse(validRequest2, validResponse2); - expect(response.constructor).toBe(AuthStatusResponse); - expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalled(); - }); - - it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => { - const response = service.parse(validRequest, validResponse1); - expect(response.constructor).toBe(AuthStatusResponse); - }); - - }); -}); diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts deleted file mode 100644 index 8c77770974..0000000000 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; - -import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { AuthStatusResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseParsingService } from '../data/parsing.service'; -import { RestRequest } from '../data/request.models'; -import { AuthStatus } from './models/auth-status.model'; - -@Injectable() -export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected toCache = true; - - constructor(protected objectCache: ObjectCacheService) { - super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { - const response = this.process(data.payload, request); - return new AuthStatusResponse(response, data.statusCode, data.statusText); - } else { - return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText); - } - } -} diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index be40351795..5bfaaa323b 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -8,14 +8,14 @@ import { of as observableOf } from 'rxjs'; import { AuthInterceptor } from './auth.interceptor'; import { AuthService } from './auth.service'; -import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { RouterStub } from '../../shared/testing/router.stub'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RestRequestMethod } from '../data/rest-request-method'; describe(`AuthInterceptor`, () => { - let service: DSpaceRESTv2Service; + let service: DspaceRestService; let httpMock: HttpTestingController; const authServiceStub = new AuthServiceStub(); @@ -30,7 +30,7 @@ describe(`AuthInterceptor`, () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ - DSpaceRESTv2Service, + DspaceRestService, {provide: AuthService, useValue: authServiceStub}, {provide: Router, useClass: RouterStub}, { @@ -42,7 +42,7 @@ describe(`AuthInterceptor`, () => { ], }); - service = TestBed.get(DSpaceRESTv2Service); + service = TestBed.get(DspaceRestService); httpMock = TestBed.get(HttpTestingController); }); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 8eea9f8938..51c4a6cbf3 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -10,7 +10,7 @@ import { CookieAttributes } from 'js-cookie'; import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { @@ -44,6 +44,7 @@ import { EPersonDataService } from '../eperson/eperson-data.service'; import { getAllSucceededRemoteDataPayload } from '../shared/operators'; import { AuthMethod } from './models/auth.method'; import { HardRedirectService } from '../services/hard-redirect.service'; +import { RemoteData } from '../data/remote-data'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -94,9 +95,9 @@ export class AuthService { headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); options.headers = headers; return this.authRequestService.postToEndpoint('login', body, options).pipe( - map((status: AuthStatus) => { - if (status.authenticated) { - return status; + map((rd: RemoteData) => { + if (hasValue(rd.payload) && rd.payload.authenticated) { + return rd.payload; } else { throw(new Error('Invalid email or password')); } @@ -115,7 +116,7 @@ export class AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((status: AuthStatus) => Object.assign(new AuthStatus(), status)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) ); } @@ -147,8 +148,9 @@ export class AuthService { headers = headers.append('Authorization', `Bearer ${token.accessToken}`); options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status: AuthStatus) => { - if (status.authenticated) { + map((rd: RemoteData) => { + const status = rd.payload; + if (hasValue(status) && status.authenticated) { return status._links.eperson.href; } else { throw(new Error('Not authenticated')); @@ -229,8 +231,9 @@ export class AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.postToEndpoint('login', {}, options).pipe( - map((status: AuthStatus) => { - if (status.authenticated) { + map((rd: RemoteData) => { + const status = rd.payload; + if (hasValue(status) && status.authenticated) { return status.token; } else { throw(new Error('Not authenticated')); @@ -267,8 +270,9 @@ export class AuthService { 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( - map((status: AuthStatus) => { - if (!status.authenticated) { + map((rd: RemoteData) => { + const status = rd.payload; + if (hasValue(status) && !status.authenticated) { return true; } else { throw(new Error('auth.errors.invalid-user')); diff --git a/src/app/core/auth/models/short-lived-token.model.ts b/src/app/core/auth/models/short-lived-token.model.ts new file mode 100644 index 0000000000..e51f35fd58 --- /dev/null +++ b/src/app/core/auth/models/short-lived-token.model.ts @@ -0,0 +1,35 @@ +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { autoserialize, deserialize, autoserializeAs } from 'cerialize'; +import { ResourceType } from '../../shared/resource-type'; +import { SHORT_LIVED_TOKEN } from './short-lived-token.resource-type'; +import { HALLink } from '../../shared/hal-link.model'; + +/** + * A short-lived token that can be used to authenticate a rest request + */ +@typedObject +export class ShortLivedToken implements CacheableObject { + static type = SHORT_LIVED_TOKEN; + /** + * The type for this ShortLivedToken + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The value for this ShortLivedToken + */ + @autoserializeAs('token') + value: string; + + /** + * The {@link HALLink}s for this ShortLivedToken + */ + @deserialize + _links: { + self: HALLink; + }; +} diff --git a/src/app/core/auth/models/short-lived-token.resource-type.ts b/src/app/core/auth/models/short-lived-token.resource-type.ts new file mode 100644 index 0000000000..608c1aca4e --- /dev/null +++ b/src/app/core/auth/models/short-lived-token.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for ShortLivedToken + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SHORT_LIVED_TOKEN = new ResourceType('shortlivedtoken'); diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 88a4ac406e..9840b22267 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -4,11 +4,12 @@ import { HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { isNotEmpty } from '../../shared/empty.util'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { isNotEmpty, hasValue } from '../../shared/empty.util'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; +import { RemoteData } from '../data/remote-data'; /** * The auth service. @@ -30,8 +31,9 @@ export class ServerAuthService extends AuthService { options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status: AuthStatus) => { - if (status.authenticated) { + map((rd: RemoteData) => { + const status = rd.payload; + if (hasValue(status) && status.authenticated) { return status._links.eperson.href; } else { throw(new Error('Not authenticated')); @@ -55,7 +57,7 @@ export class ServerAuthService extends AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((status: AuthStatus) => Object.assign(new AuthStatus(), status)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) ); } } diff --git a/src/app/core/auth/token-response-parsing.service.spec.ts b/src/app/core/auth/token-response-parsing.service.spec.ts index 35927708f6..a440325560 100644 --- a/src/app/core/auth/token-response-parsing.service.spec.ts +++ b/src/app/core/auth/token-response-parsing.service.spec.ts @@ -1,5 +1,5 @@ import { TokenResponseParsingService } from './token-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { TokenResponse } from '../cache/response.models'; describe('TokenResponseParsingService', () => { @@ -17,7 +17,7 @@ describe('TokenResponseParsingService', () => { }, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; + } as RawRestResponse; const expected = new TokenResponse(data.payload.token, true, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); }); @@ -27,7 +27,7 @@ describe('TokenResponseParsingService', () => { payload: {}, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; + } as RawRestResponse; const expected = new TokenResponse(null, false, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); }); @@ -37,7 +37,7 @@ describe('TokenResponseParsingService', () => { payload: {}, statusCode: 400, statusText: 'BAD REQUEST' - } as DSpaceRESTV2Response; + } as RawRestResponse; const expected = new TokenResponse(null, false, 400, 'BAD REQUEST'); expect(service.parse(undefined, data)).toEqual(expected); }); diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts index a1b1e23aa4..30abe01c87 100644 --- a/src/app/core/auth/token-response-parsing.service.ts +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -1,18 +1,18 @@ import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RestResponse, TokenResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; import { Injectable } from '@angular/core'; @Injectable() /** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string + * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a token string * wrapped in a TokenResponse */ export class TokenResponseParsingService implements ResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + parse(request: RestRequest, data: RawRestResponse): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) { return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText); } else { diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 03d4db3f5d..49c7aed365 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -3,8 +3,8 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DataService } from '../data/data.service'; -import { getRemoteDataPayload } from '../shared/operators'; -import { filter, map, take } from 'rxjs/operators'; +import { getRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators'; +import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { DSpaceObject } from '../shared/dspace-object.model'; import { ChildHALResource } from '../shared/child-hal-resource.model'; @@ -29,9 +29,8 @@ export abstract class DSOBreadcrumbResolver> { const uuid = route.params.id; - return this.dataService.findById(uuid, ...this.followLinks).pipe( - filter((rd) => hasValue(rd.error) || hasValue(rd.payload)), - take(1), + return this.dataService.findById(uuid, false, ...this.followLinks).pipe( + getFirstCompletedRemoteData(), getRemoteDataPayload(), map((object: T) => { if (hasValue(object)) { diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts new file mode 100644 index 0000000000..06fea5b870 --- /dev/null +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -0,0 +1,47 @@ +import { BrowseDefinitionDataService } from './browse-definition-data.service'; +import { FindListOptions } from '../data/request.models'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { EMPTY } from 'rxjs'; + +describe(`BrowseDefinitionDataService`, () => { + let service: BrowseDefinitionDataService; + const dataServiceImplSpy = jasmine.createSpyObj('dataService', { + findAll: EMPTY, + findByHref: EMPTY, + findAllByHref: EMPTY, + }); + const hrefAll = 'https://rest.api/server/api/discover/browses'; + const hrefSingle = 'https://rest.api/server/api/discover/browses/author'; + const options = new FindListOptions(); + const linksToFollow = [ + followLink('entries'), + followLink('items') + ] + + beforeEach(() => { + service = new BrowseDefinitionDataService(null, null, null, null, null, null, null, null); + (service as any).dataService = dataServiceImplSpy; + }); + + describe(`findAll`, () => { + it(`should call findAll on DataServiceImpl`, () => { + service.findAll(options, false, ...linksToFollow); + expect(dataServiceImplSpy.findAll).toHaveBeenCalledWith(options, false, ...linksToFollow); + }); + }); + + describe(`findByHref`, () => { + it(`should call findByHref on DataServiceImpl`, () => { + service.findByHref(hrefSingle, false, ...linksToFollow); + expect(dataServiceImplSpy.findByHref).toHaveBeenCalledWith(hrefSingle, false, ...linksToFollow); + }); + }); + + describe(`findAllByHref`, () => { + it(`should call findAllByHref on DataServiceImpl`, () => { + service.findAllByHref(hrefAll, options, false, ...linksToFollow); + expect(dataServiceImplSpy.findAllByHref).toHaveBeenCalledWith(hrefAll, options, false, ...linksToFollow); + }); + }); + +}); diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts new file mode 100644 index 0000000000..2bd88c12f9 --- /dev/null +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; +import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; +import { DataService } from '../data/data.service'; +import { BrowseDefinition } from '../shared/browse-definition.model'; +import { RequestService } from '../data/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 '../data/default-change-analyzer.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../data/remote-data'; +import { FindListOptions } from '../data/request.models'; +import { PaginatedList } from '../data/paginated-list.model'; + +/* tslint:disable:max-classes-per-file */ + +class DataServiceImpl extends DataService { + protected linkPath = 'browses'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +@Injectable({ + providedIn: 'root' +}) +@dataService(BROWSE_DEFINITION) +export class BrowseDefinitionDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns {@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 + * @return {Observable>>} + * Return an observable that emits object list + */ + findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAll(options, 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 + */ + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, 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 + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); + } +} + +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 58796636e2..790f260d5d 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -5,12 +5,14 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-bu import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; 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'; describe('BrowseService', () => { let scheduler: TestScheduler; @@ -79,6 +81,8 @@ describe('BrowseService', () => { }) ]; + let browseDefinitionDataService; + const getRequestEntry$ = (successful: boolean) => { return observableOf({ response: { isSuccessful: successful, payload: browseDefinitions } as any @@ -86,9 +90,13 @@ describe('BrowseService', () => { }; function initTestService() { + browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)) + }); return new BrowseService( requestService, halService, + browseDefinitionDataService, rdbService ); } @@ -105,35 +113,14 @@ describe('BrowseService', () => { service = initTestService(); spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); - spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + spyOn(rdbService, 'buildList').and.callThrough(); }); - it('should configure a new BrowseEndpointRequest', () => { - const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL); - - scheduler.schedule(() => service.getBrowseDefinitions().subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => { service.getBrowseDefinitions(); - - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + expect(browseDefinitionDataService.findAll).toHaveBeenCalled(); }); - - it('should return a RemoteData object containing the correct BrowseDefinition[]', () => { - const expected = cold('--a-', { - a: { - payload: browseDefinitions - } - }); - - expect(service.getBrowseDefinitions()).toBeObservable(expected); - }); - }); describe('getBrowseEntriesFor and getBrowseItemsFor', () => { @@ -145,16 +132,14 @@ describe('BrowseService', () => { service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { - a: { - payload: browseDefinitions - } + a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) })); - spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + 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 BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href); + const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href); scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); @@ -165,7 +150,7 @@ describe('BrowseService', () => { it('should call RemoteDataBuildService to create the RemoteData Observable', () => { service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)); - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + expect(rdbService.buildList).toHaveBeenCalled(); }); @@ -173,7 +158,7 @@ describe('BrowseService', () => { describe('when getBrowseItemsFor is called with a valid browse definition id', () => { it('should configure a new BrowseItemsRequest', () => { - const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName); + const expected = new GetRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); @@ -184,31 +169,11 @@ describe('BrowseService', () => { it('should call RemoteDataBuildService to create the RemoteData Observable', () => { service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)); - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + expect(rdbService.buildList).toHaveBeenCalled(); }); }); - - describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => { - it('should throw an Error', () => { - - const definitionID = 'invalidID'; - const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)); - - expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected); - }); - }); - - describe('when getBrowseItemsFor is called with an invalid browse definition id', () => { - it('should throw an Error', () => { - - const definitionID = 'invalidID'; - const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)) - - expect(service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected); - }); - }); }); describe('getBrowseURLFor', () => { @@ -220,9 +185,7 @@ describe('BrowseService', () => { service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { - a: { - payload: browseDefinitions - } + a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) })); }); @@ -295,18 +258,16 @@ describe('BrowseService', () => { service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { - a: { - payload: browseDefinitions - } + a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) })); - spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + 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 BrowseItemsRequest(requestService.generateRequestId(), expectedURL); + const expected = new GetRequest(requestService.generateRequestId(), expectedURL); scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe()); scheduler.flush(); @@ -317,7 +278,7 @@ describe('BrowseService', () => { it('should call RemoteDataBuildService to create the RemoteData Observable', () => { service.getFirstItemFor(browseDefinitions[1].id); - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + expect(rdbService.buildList).toHaveBeenCalled(); }); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 78e63e8540..fb52cd8d05 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,35 +1,26 @@ import { Injectable } from '@angular/core'; import { Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { - ensureArrayHasValue, - hasValue, - hasValueOperator, - isEmpty, - isNotEmpty, - isNotEmptyOperator -} from '../../shared/empty.util'; +import { distinctUntilChanged, map, startWith, take } from 'rxjs/operators'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { GenericSuccessResponse } from '../cache/response.models'; -import { PaginatedList } from '../data/paginated-list'; +import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; -import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest, RestRequest } from '../data/request.models'; +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'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { - configureRequest, - filterSuccessfulResponses, getBrowseDefinitionLinks, getFirstOccurrence, getRemoteDataPayload, - getRequestFromRequestHref + getFirstSucceededRemoteData, + getPaginatedListPayload } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; +import { BrowseDefinitionDataService } from './browse-definition-data.service'; /** * The service handling all browse requests @@ -54,6 +45,7 @@ export class BrowseService { constructor( protected requestService: RequestService, protected halService: HALEndpointService, + private browseDefinitionDataService: BrowseDefinitionDataService, private rdb: RemoteDataBuildService, ) { } @@ -61,27 +53,11 @@ export class BrowseService { /** * Get all BrowseDefinitions */ - getBrowseDefinitions(): Observable> { - const request$ = this.halService.getEndpoint(this.linkPath).pipe( - isNotEmptyOperator(), - distinctUntilChanged(), - map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)), - configureRequest(this.requestService) + getBrowseDefinitions(): Observable>> { + // TODO properly support pagination + return this.browseDefinitionDataService.findAll({ elementsPerPage: 9999 }).pipe( + getFirstSucceededRemoteData(), ); - - const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); - const payload$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: GenericSuccessResponse) => response.payload), - ensureArrayHasValue(), - map((definitions: BrowseDefinition[]) => definitions - .map((definition: BrowseDefinition) => { - return Object.assign(new BrowseDefinition(), definition) - })), - distinctUntilChanged(), - ); - return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } /** @@ -191,6 +167,7 @@ export class BrowseService { return href; }), getBrowseItemsFor(this.requestService, this.rdb), + getFirstSucceededRemoteData(), getFirstOccurrence() ); } @@ -244,6 +221,7 @@ export class BrowseService { const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey); return this.getBrowseDefinitions().pipe( getRemoteDataPayload(), + getPaginatedListPayload(), map((browseDefinitions: BrowseDefinition[]) => browseDefinitions .find((def: BrowseDefinition) => { const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0); @@ -271,12 +249,16 @@ export class BrowseService { * @param rdb */ export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => - source.pipe( - map((href: string) => new BrowseEntriesRequest(requestService.generateRequestId(), href)), - configureRequest(requestService), - toRDPaginatedBrowseEntries(requestService, rdb) - ); + (source: Observable): Observable>> => { + const requestId = requestService.generateRequestId(); + + source.pipe(take(1)).subscribe((href: string) => { + const request = new GetRequest(requestId, href); + requestService.configure(request); + }) + + return rdb.buildList(source); + }; /** * Operator for turning a href into a PaginatedList of Items @@ -285,57 +267,13 @@ export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteD * @param rdb */ export const getBrowseItemsFor = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => - source.pipe( - map((href: string) => new BrowseItemsRequest(requestService.generateRequestId(), href)), - configureRequest(requestService), - toRDPaginatedBrowseItems(requestService, rdb) - ); + (source: Observable): Observable>> => { + const requestId = requestService.generateRequestId(); -/** - * Operator for turning a RestRequest into a PaginatedList of Items - * @param requestService - * @param responseCache - * @param rdb - */ -export const toRDPaginatedBrowseItems = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => { - const href$ = source.pipe(map((request: RestRequest) => request.href)); + source.pipe(take(1)).subscribe((href: string) => { + const request = new GetRequest(requestId, href); + requestService.configure(request); + }) - const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService)); - - const payload$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), - map((list: PaginatedList) => Object.assign(list, { - page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page - })), - distinctUntilChanged() - ); - - return rdb.toRemoteDataObservable(requestEntry$, payload$); - }; - -/** - * Operator for turning a RestRequest into a PaginatedList of BrowseEntries - * @param requestService - * @param responseCache - * @param rdb - */ -export const toRDPaginatedBrowseEntries = (requestService: RequestService, rdb: RemoteDataBuildService) => - (source: Observable): Observable>> => { - const href$ = source.pipe(map((request: RestRequest) => request.href)); - - const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService)); - - const payload$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), - map((list: PaginatedList) => Object.assign(list, { - page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page - })), - distinctUntilChanged() - ); - - return rdb.toRemoteDataObservable(requestEntry$, payload$); + return rdb.buildList(source); }; diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 06a955cb00..af4c4fdf36 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -3,7 +3,11 @@ import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import { CacheableObject, TypedObject } from '../object-cache.reducer'; +import { + CacheableObject, + TypedObject, + getResourceTypeValueFor +} from '../object-cache.reducer'; const resolvedLinkKey = Symbol('resolvedLink'); @@ -14,9 +18,9 @@ const linkMap = new Map(); /** * Decorator function to map a ResourceType to its class - * @param target The contructor of the typed class to map + * @param target the typed class to map */ -export function typedObject(target: typeof TypedObject) { +export function typedObject(target: TypedObject) { typeMap.set(target.type.value, target); } @@ -25,10 +29,7 @@ export function typedObject(target: typeof TypedObject) { * @param type The resource type */ export function getClassForType(type: string | ResourceType) { - if (typeof(type) === 'object') { - type = (type as ResourceType).value; - } - return typeMap.get(type); + return typeMap.get(getResourceTypeValueFor(type)); } /** diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 871be7e9ae..b7db0ebb30 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -38,10 +38,10 @@ class TestModel implements HALResource { @Injectable() class TestDataService { - findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>) { + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>) { return 'findAllByHref'; } - findByHref(href: string, ...linksToFollow: Array>) { + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>) { return 'findByHref'; } } @@ -92,7 +92,7 @@ describe('LinkService', () => { service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor'))) }); it('should call dataservice.findByHref with the correct href and nested links', () => { - expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, followLink('successor')); + expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, followLink('successor')); }); }); describe(`when the linkdefinition concerns a list`, () => { @@ -107,7 +107,7 @@ describe('LinkService', () => { service.resolveLink(testModel, followLink('predecessor', { some: 'options '} as any, 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, followLink('successor')); + expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options '} as any, true, followLink('successor')); }); }); describe('either way', () => { diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index 8d0def71fc..e5f1d50b79 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -61,12 +61,13 @@ export class LinkService { try { if (matchingLinkDef.isList) { - model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, true, ...linkToFollow.linksToFollow); } else { - model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + model[linkToFollow.name] = service.findByHref(href, true, ...linkToFollow.linksToFollow); } } catch (e) { - throw new Error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} from ${href}`); + console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`); + throw e; } } } diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index 8bf689d794..da7681d587 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -1,63 +1,647 @@ -import { of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { PaginatedList } from '../../data/paginated-list'; -import { RemoteData } from '../../data/remote-data'; +import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model'; import { Item } from '../../shared/item.model'; import { PageInfo } from '../../shared/page-info.model'; import { RemoteDataBuildService } from './remote-data-build.service'; - -const pageInfo = new PageInfo(); -const array = [ - Object.assign(new Item(), { - metadata: { - 'dc.title': [ - { - language: 'en_US', - value: 'Item nr 1' - } - ] - } - }), - Object.assign(new Item(), { - metadata: { - 'dc.title': [ - { - language: 'en_US', - value: 'Item nr 2' - } - ] - } - }) -]; -const paginatedList = new PaginatedList(pageInfo, array); -const arrayRD = createSuccessfulRemoteDataObject(array); -const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); +import { ObjectCacheService } from '../object-cache.service'; +import { ITEM } from '../../shared/item.resource-type'; +import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { LinkService } from './link.service'; +import { RequestService } from '../../data/request.service'; +import { UnCacheableObject } from '../../shared/uncacheable-object.model'; +import { RemoteData } from '../../data/remote-data'; +import { Observable, of as observableOf } from 'rxjs'; +import { RequestEntry, RequestEntryState } from '../../data/request.reducer'; +import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; +import { take } from 'rxjs/operators'; +import { HALLink } from '../../shared/hal-link.model'; describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let linkService: LinkService; + let requestService: RequestService; + let unCacheableObject: UnCacheableObject; + let pageInfo: PageInfo; + let paginatedList: PaginatedList; + let normalizedPaginatedList: PaginatedList; + let selfLink1: string; + let selfLink2: string; + let pageLinks: HALLink[]; + let array: Item[]; + let arrayRD: RemoteData; + let paginatedListRD: RemoteData>; + let requestEntry$: Observable; + let entrySuccessCacheable: RequestEntry; + let entrySuccessUnCacheable: RequestEntry; + let entrySuccessNoContent: RequestEntry; + let entryError: RequestEntry; + let linksToFollow: Array>; beforeEach(() => { - service = new RemoteDataBuildService(undefined, undefined, undefined); + objectCache = getMockObjectCacheService(); + linkService = getMockLinkService(); + requestService = getMockRequestService(); + unCacheableObject = { + foo: 'bar' + } + pageInfo = new PageInfo(); + selfLink1 = 'https://rest.api/some/object'; + selfLink2 = 'https://rest.api/another/object'; + pageLinks = [ + { href: selfLink1 }, + { href: selfLink2 }, + ] + array = [ + Object.assign(new Item(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Item nr 1' + } + ] + }, + _links: { + self: { + href: selfLink1 + } + } + }), + Object.assign(new Item(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Item nr 2' + } + ] + }, + _links: { + self: { + href: selfLink2 + } + } + }) + ]; + paginatedList = buildPaginatedList(pageInfo, array); + normalizedPaginatedList = buildPaginatedList(pageInfo, array, true); + arrayRD = createSuccessfulRemoteDataObject(array); + paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + entrySuccessCacheable = { + request: { + uuid: '17820127-0ee5-4ed4-b6da-e654bdff8487' + }, + state: RequestEntryState.Success, + response: { + statusCode: 200, + payloadLink: { + href: selfLink1 + } + } + } as RequestEntry; + entrySuccessUnCacheable = { + request: { + uuid: '0aa5ec06-d6a7-4e73-952e-1e0462bd1501' + }, + state: RequestEntryState.Success, + response: { + statusCode: 200, + unCacheableObject, + } + } as RequestEntry; + entrySuccessNoContent = { + request: { + uuid: '780a7295-6102-4a43-9775-80f2a4ff673c' + }, + state: RequestEntryState.Success, + response: { + statusCode: 204 + }, + } as RequestEntry; + entryError = { + request: { + uuid: '1609dcbc-8442-4877-966e-864f151cc40c' + }, + state: RequestEntryState.Error, + response: { + statusCode: 500, + } + } as RequestEntry; + requestEntry$ = observableOf(entrySuccessCacheable); + linksToFollow = [ + followLink('a'), + followLink('b'), + ] + + service = new RemoteDataBuildService(objectCache, linkService, requestService); }); - describe('when toPaginatedList is called', () => { - let expected: RemoteData>; - + describe(`buildPayload`, () => { beforeEach(() => { - expected = paginatedListRD; + spyOn(service as any, 'plainObjectToInstance').and.returnValue(unCacheableObject); + spyOn(service as any, 'buildPaginatedList').and.returnValue(observableOf(paginatedList)); + (objectCache.getObjectByHref as jasmine.Spy).and.returnValue(observableOf(array[0])); + (linkService.resolveLinks as jasmine.Spy).and.returnValue(array[1]); }); - it('should return the correct remoteData of a paginatedList when the input is a (remoteData of an) array', () => { - const result = (service as any).toPaginatedList(observableOf(arrayRD), pageInfo); - result.subscribe((resultRD) => { - expect(resultRD).toEqual(expected); + describe(`when no self link for the object to retrieve is provided`, () => { + beforeEach(() => { + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(false); + spyOn(service as any, 'isCacheablePayload').and.returnValue(true); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(false); + }); + + it(`should call hasExactMatchInObjectCache with undefined and the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).hasExactMatchInObjectCache).toHaveBeenCalledWith(undefined, entrySuccessCacheable); + done(); + }); + }); + + it(`should call isCacheablePayload with the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).isCacheablePayload).toHaveBeenCalledWith(entrySuccessCacheable); + done(); + }); + }); + + it(`should not call isUnCacheablePayload`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).isUnCacheablePayload).not.toHaveBeenCalled(); + done(); + }); + }); + + it(`should call objectCache.getObjectByHref() with the payloadLink from the response`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(objectCache.getObjectByHref).toHaveBeenCalledWith(entrySuccessCacheable.response.payloadLink.href); + done(); + }); + }); + + it(`should call linkService.resolveLinks with the object from the cache and the linksToFollow`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(array[0], ...linksToFollow); + done(); + }); + }); + + it(`should return the object returned from linkService.resolveLinks`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toEqual(array[1]); + done(); + }); + }); + + }); + + describe(`when a self link for the object to retrieve is provided`, () => { + beforeEach(() => { + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(true); + spyOn(service as any, 'isCacheablePayload').and.returnValue(false); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(false); + }); + + it(`should call hasExactMatchInObjectCache with that self link and the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, observableOf(selfLink2), ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).hasExactMatchInObjectCache).toHaveBeenCalledWith(selfLink2, entrySuccessCacheable); + done(); + }); + }); + + it(`should call objectCache.getObjectByHref() with that self link`, (done) => { + (service as any).buildPayload(requestEntry$, observableOf(selfLink2), ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(objectCache.getObjectByHref).toHaveBeenCalledWith(selfLink2); + done(); + }); + }); + + it(`should call linkService.resolveLinks with the object from the cache and the linksToFollow`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(array[0], ...linksToFollow); + done(); + }); + }); + + it(`should return the object returned from linkService.resolveLinks`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toEqual(array[1]); + done(); + }); }); }); - it('should return the correct remoteData of a paginatedList when the input is a (remoteData of a) paginated list', () => { - const result = (service as any).toPaginatedList(observableOf(paginatedListRD), pageInfo); - result.subscribe((resultRD) => { - expect(resultRD).toEqual(expected); + describe(`when the entry contains an uncachable payload`, () => { + beforeEach(() => { + requestEntry$ = observableOf(entrySuccessUnCacheable); + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(false); + spyOn(service as any, 'isCacheablePayload').and.returnValue(false); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(true); + }); + + it(`should call hasExactMatchInObjectCache with undefined and the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).hasExactMatchInObjectCache).toHaveBeenCalledWith(undefined, entrySuccessUnCacheable); + done(); + }); + }); + + it(`should call isCacheablePayload with the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).isCacheablePayload).toHaveBeenCalledWith(entrySuccessUnCacheable); + done(); + }); + }); + + it(`should call isUnCacheablePayload with the requestEntry`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).isUnCacheablePayload).toHaveBeenCalledWith(entrySuccessUnCacheable); + done(); + }); + }); + + it(`should call plainObjectToInstance with the unCacheableObject from the response`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).plainObjectToInstance).toHaveBeenCalledWith(unCacheableObject); + done(); + }); + }); + + it(`should return the uncacheable object from the response`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toBe(unCacheableObject); + done(); + }); + }); + }); + + describe(`when the entry contains a 204 response`, () => { + beforeEach(() => { + requestEntry$ = observableOf(entrySuccessNoContent); + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(false); + spyOn(service as any, 'isCacheablePayload').and.returnValue(false); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(false); + }); + + it(`should return null`, (done) => { + (service as any).buildPayload(requestEntry$, observableOf(selfLink2), ...linksToFollow) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toBeNull(); + done(); + }); + }); + }); + + describe(`when the entry contains an error`, () => { + beforeEach(() => { + requestEntry$ = observableOf(entryError); + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(false); + spyOn(service as any, 'isCacheablePayload').and.returnValue(false); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(false); + }); + + it(`should return undefined`, (done) => { + (service as any).buildPayload(requestEntry$, undefined) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toBeUndefined(); + done(); + }); + }); + }); + + describe(`when the entry contains a link to a paginated list`, () => { + beforeEach(() => { + requestEntry$ = observableOf(entrySuccessCacheable); + (objectCache.getObjectByHref as jasmine.Spy).and.returnValue(observableOf(paginatedList)); + spyOn(service as any, 'hasExactMatchInObjectCache').and.returnValue(false); + spyOn(service as any, 'isCacheablePayload').and.returnValue(true); + spyOn(service as any, 'isUnCacheablePayload').and.returnValue(false); + }); + + it(`should call buildPaginatedList with the object from the cache and the linksToFollow`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect((service as any).buildPaginatedList).toHaveBeenCalledWith(paginatedList, ...linksToFollow); + done(); + }); + }); + + it(`should return the paginated list`, (done) => { + (service as any).buildPayload(requestEntry$, undefined, ...linksToFollow) + .pipe(take(1)) + .subscribe((response) => { + expect(response).toEqual(paginatedList); + done(); + }); + }); + }); + + }); + + describe('hasExactMatchInObjectCache', () => { + describe('when the object-cache contains the href, and it has a reference to the request id', () => { + beforeEach(() => { + (objectCache.hasByHref as jasmine.Spy).and.returnValue(true); + }); + + it('should return true if the href has a value, and the entry has a request with a valid id', () => { + expect((service as any).hasExactMatchInObjectCache('href', entrySuccessCacheable)).toEqual(true); + }); + + it('should return false if the href is undefined', () => { + expect((service as any).hasExactMatchInObjectCache(undefined, entrySuccessCacheable)).toEqual(false); + }); + + it('should return false if the requestEntry is undefined', () => { + expect((service as any).hasExactMatchInObjectCache('href', undefined)).toEqual(false); + }); + + it('should return false if the requestEntry has no request', () => { + expect((service as any).hasExactMatchInObjectCache('href', {})).toEqual(false); + }); + + it(`should return false if the requestEntry's request has no id`, () => { + expect((service as any).hasExactMatchInObjectCache('href', { request: {}})).toEqual(false); + }); + }); + + describe('when the object-cache doesn\'t contain the href', () => { + beforeEach(() => { + (objectCache.hasByHref as jasmine.Spy).and.returnValue(false); + }); + + it('should return false if the href has a value', () => { + expect((service as any).hasExactMatchInObjectCache('href')).toEqual(false); + }); + + it('should return false if the href is undefined', () => { + expect((service as any).hasExactMatchInObjectCache(undefined)).toEqual(false); + }); + }); + }); + + describe('isCacheablePayload', () => { + let entry; + + describe('when the entry\'s response contains a cacheable payload', () => { + beforeEach(() => { + entry = { + response: { + payloadLink: { href: 'payload-link' } + } + } + }); + + it('should return true', () => { + expect((service as any).isCacheablePayload(entry)).toEqual(true); + }); + }); + + describe('when the entry\'s response doesn\'t contain a cacheable payload', () => { + beforeEach(() => { + entry = { + response: { + payloadLink: undefined + } + } + }); + + it('should return false', () => { + expect((service as any).isCacheablePayload(entry)).toEqual(false); + }); + }); + }); + + describe('isUnCacheablePayload', () => { + let entry; + + describe('when the entry\'s response contains an uncacheable object', () => { + beforeEach(() => { + entry = { + response: { + unCacheableObject: Object.assign({}) + } + } + }); + + it('should return true', () => { + expect((service as any).isUnCacheablePayload(entry)).toEqual(true); + }); + }); + + describe('when the entry\'s response doesn\'t contain an uncacheable object', () => { + beforeEach(() => { + entry = { + response: {} + } + }); + + it('should return false', () => { + expect((service as any).isUnCacheablePayload(entry)).toEqual(false); + }); + }); + }); + + describe(`plainObjectToInstance`, () => { + describe(`when the object has a recognized type property`, () => { + it(`should return a new instance of that type`, () => { + const source: any = { + type: ITEM, + uuid: 'some-uuid' + }; + + const result = (service as any).plainObjectToInstance(source); + result.foo = 'bar' + + expect(result).toEqual(jasmine.any(Item)); + expect(result.uuid).toEqual('some-uuid'); + expect(result.foo).toEqual('bar'); + expect(source.foo).toBeUndefined(); + }); + }); + describe(`when the object doesn't have a recognized type property`, () => { + it(`should return a new plain JS object`, () => { + const source: any = { + type: 'foobar', + uuid: 'some-uuid' + }; + + const result = (service as any).plainObjectToInstance(source); + result.foo = 'bar' + + expect(result).toEqual(jasmine.any(Object)); + expect(result.uuid).toEqual('some-uuid'); + expect(result.foo).toEqual('bar'); + expect(source.foo).toBeUndefined(); + }); + }); + }); + + describe(`buildPaginatedList`, () => { + beforeEach(() => { + (objectCache.getList as jasmine.Spy).and.returnValue(observableOf(array)); + (linkService.resolveLinks as jasmine.Spy).and.callFake((obj) => obj); + spyOn(service as any, 'plainObjectToInstance').and.callFake((obj) => obj); + }); + describe(`when linksToFollow contains a 'page' link`, () => { + let paginatedLinksToFollow; + beforeEach(() => { + paginatedLinksToFollow = [ + followLink('page', undefined, true, ...linksToFollow), + ...linksToFollow + ] + }); + describe(`and the given list doesn't have a page property already`, () => { + it(`should call objectCache.getList with the links in _links.page of the given object`, (done) => { + (service as any).buildPaginatedList(normalizedPaginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(objectCache.getList).toHaveBeenCalledWith(pageLinks.map((link: HALLink) => link.href)) + done(); + }) + }); + + it(`should call plainObjectToInstance for each of the page objects`, (done) => { + (service as any).buildPaginatedList(normalizedPaginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + array.forEach((element) => { + expect((service as any).plainObjectToInstance).toHaveBeenCalledWith(element); + }) + done(); + }) + }); + + it(`should call linkService.resolveLinks for each of the page objects`, (done) => { + (service as any).buildPaginatedList(normalizedPaginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + array.forEach((element) => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(element, ...linksToFollow); + }) + done(); + }) + }); + + it(`should return a new PaginatedList instance based on the given object`, (done) => { + const listAsPlainJSObj = Object.assign({}, normalizedPaginatedList); + (service as any).buildPaginatedList(listAsPlainJSObj, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe((result) => { + expect(listAsPlainJSObj).toEqual(jasmine.any(Object)); + expect(result).toEqual(jasmine.any(PaginatedList)); + expect(result).toEqual(paginatedList); + done(); + }) + }); + + describe(`when there are other links as well`, () => { + it(`should call linkservice.resolveLinks for those other links`, (done) => { + (service as any).buildPaginatedList(normalizedPaginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(paginatedList, ...linksToFollow); + done(); + }) + }); + }); + }); + + describe(`and the given list already has a page property`, () => { + it(`should call plainObjectToInstance for each of the page objects`, (done) => { + (service as any).buildPaginatedList(paginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + array.forEach((element) => { + expect((service as any).plainObjectToInstance).toHaveBeenCalledWith(element); + }) + done(); + }) + }); + + it(`should call linkService.resolveLinks for each of the page objects`, (done) => { + (service as any).buildPaginatedList(paginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + array.forEach((element) => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(element, ...linksToFollow); + }) + done(); + }) + }); + + it(`should return a new PaginatedList instance based on the given object`, (done) => { + const listAsPlainJSObj = Object.assign({}, paginatedList); + (service as any).buildPaginatedList(listAsPlainJSObj, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe((result) => { + expect(listAsPlainJSObj).toEqual(jasmine.any(Object)); + expect(result).toEqual(jasmine.any(PaginatedList)); + expect(result).toEqual(paginatedList); + done(); + }) + }); + + describe(`when there are other links as well`, () => { + it(`should call linkservice.resolveLinks for those other links`, (done) => { + (service as any).buildPaginatedList(paginatedList, ...paginatedLinksToFollow) + .pipe(take(1)) + .subscribe(() => { + expect(linkService.resolveLinks).toHaveBeenCalledWith(paginatedList, ...linksToFollow); + done(); + }) + }); + }); + }); + }); + + describe(`when linksToFollow doesn't contain a 'page' link`, () => { + it(`should return a new PaginatedList instance based on the given object`, (done) => { + const listAsPlainJSObj = Object.assign({}, paginatedList); + (service as any).buildPaginatedList(listAsPlainJSObj, ...linksToFollow) + .pipe(take(1)) + .subscribe((result) => { + expect(listAsPlainJSObj).toEqual(jasmine.any(Object)); + expect(result).toEqual(jasmine.any(PaginatedList)); + expect(result).toEqual(paginatedList); + done(); + }) }); }); }); diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 83cecca502..cb79a64091 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,20 +1,33 @@ import { Injectable } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; -import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + race as observableRace +} from 'rxjs'; +import { map, switchMap, filter, distinctUntilKeyChanged } from 'rxjs/operators'; +import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { PaginatedList } from '../../data/paginated-list'; +import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; -import { RemoteDataError } from '../../data/remote-data-error'; -import { RequestEntry } from '../../data/request.reducer'; +import { + RequestEntry, + ResponseState, + RequestEntryState, + hasSucceeded +} from '../../data/request.reducer'; import { RequestService } from '../../data/request.service'; -import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; -import { PageInfo } from '../../shared/page-info.model'; -import { CacheableObject } from '../object-cache.reducer'; +import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/operators'; import { ObjectCacheService } from '../object-cache.service'; -import { DSOSuccessResponse, ErrorResponse } from '../response.models'; import { LinkService } from './link.service'; +import { HALLink } from '../../shared/hal-link.model'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { getClassForType } from './build-decorators'; +import { HALResource } from '../../shared/hal-resource.model'; +import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; +import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; +import { getResourceTypeValueFor } from '../object-cache.reducer'; @Injectable() export class RemoteDataBuildService { @@ -24,15 +37,174 @@ export class RemoteDataBuildService { } /** - * Creates a single {@link RemoteData} object based on the response of a request to the REST server, with a list of - * {@link FollowLinkConfig} that indicate which embedded info should be added to the object - * @param href$ Observable href of object we want to retrieve + * Creates an Observable with the payload for a RemoteData object + * + * @param requestEntry$ The {@link RequestEntry} to create a {@link RemoteData} object from + * @param href$ The self link of the object to retrieve. If left empty, the root + * payload link from the response will be used. These links will differ in + * case we're retrieving an object that was embedded in the request for + * another + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s + * should be automatically resolved + * @private + */ + private buildPayload(requestEntry$: Observable, href$?: Observable, ...linksToFollow: Array>): Observable { + if (hasNoValue(href$)) { + href$ = observableOf(undefined); + } + return observableCombineLatest([href$, requestEntry$]).pipe( + switchMap(([href, entry]: [string, RequestEntry]) => { + const hasExactMatchInObjectCache = this.hasExactMatchInObjectCache(href, entry); + if (hasValue(entry.response) && + (hasExactMatchInObjectCache || this.isCacheablePayload(entry) || this.isUnCacheablePayload(entry))) { + if (hasExactMatchInObjectCache) { + return this.objectCache.getObjectByHref(href); + } else if (this.isCacheablePayload(entry)) { + return this.objectCache.getObjectByHref(entry.response.payloadLink.href); + } else { + return [this.plainObjectToInstance(entry.response.unCacheableObject)]; + } + } else if (hasSucceeded(entry.state)) { + return [null]; + } else { + return [undefined]; + } + }), + switchMap((obj: T) => { + if (hasValue(obj)) { + if (getResourceTypeValueFor((obj as any).type) === PAGINATED_LIST.value) { + return this.buildPaginatedList(obj, ...linksToFollow); + } else if (isNotEmpty(linksToFollow)) { + return [this.linkService.resolveLinks(obj, ...linksToFollow)]; + } + } + return [obj]; + }) + ); + } + + /** + * When an object is returned from the store, it's possibly a plain javascript object (in case + * it was first instantiated on the server). This method will turn it in to an instance of the + * class corresponding with its type property. If it doesn't have one, or we can't find a + * constructor for that type, it will remain a plain object. + * + * @param obj The object to turn in to a class instance based on its type property + */ + private plainObjectToInstance(obj: any): T { + const type: GenericConstructor = getClassForType(obj.type); + if (typeof type === 'function') { + return Object.assign(new type(), obj) as T + } else { + return Object.assign({}, obj) as T; + } + }; + + /** + * Returns true if there is a match for the given self link and request entry in the object cache, + * false otherwise. The goal is to find objects that were not the root object of the request, but + * embedded. + * + * @param href the self link to check + * @param entry the request entry the object has to match + * @private + */ + private hasExactMatchInObjectCache(href: string, entry: RequestEntry): boolean { + return hasValue(entry) && hasValue(entry.request) && isNotEmpty(entry.request.uuid) && + hasValue(href) && this.objectCache.hasByHref(href, entry.request.uuid); + } + + /** + * Returns true if the given entry has a valid payloadLink, false otherwise + * @param entry the RequestEntry to check + * @private + */ + private isCacheablePayload(entry: RequestEntry): boolean { + return hasValue(entry.response.payloadLink) && isNotEmpty(entry.response.payloadLink.href); + } + + /** + * Returns true if the given entry has an unCacheableObject, false otherwise + * @param entry the RequestEntry to check + * @private + */ + private isUnCacheablePayload(entry: RequestEntry): boolean { + return hasValue(entry.response.unCacheableObject); + } + + /** + * Build a PaginatedList by creating a new PaginatedList instance from the given object, to ensure + * it has the correct prototype (you can't be sure if it came from the ngrx store), by + * retrieving the objects in the list and following any links. + * + * @param object A plain object to be turned in to a {@link PaginatedList} + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + private buildPaginatedList(object: any, ...linksToFollow: Array>): Observable { + const pageLink = linksToFollow.find((linkToFollow: FollowLinkConfig) => linkToFollow.name === 'page'); + const otherLinks = linksToFollow.filter((linkToFollow: FollowLinkConfig) => linkToFollow.name !== 'page'); + + const paginatedList = Object.assign(new PaginatedList(), object); + + if (hasValue(pageLink)) { + if (isEmpty(paginatedList.page)) { + const pageSelfLinks = paginatedList._links.page.map((link: HALLink) => link.href); + return this.objectCache.getList(pageSelfLinks).pipe(map((page: any[]) => { + paginatedList.page = page + .map((obj: any) => this.plainObjectToInstance(obj)) + .map((obj: any) => + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + ); + if (isNotEmpty(otherLinks)) { + return this.linkService.resolveLinks(paginatedList, ...otherLinks); + } + return paginatedList; + })); + } else { + // in case the elements of the paginated list were already filled in, because they're UnCacheableObjects + paginatedList.page = paginatedList.page + .map((obj: any) => this.plainObjectToInstance(obj)) + .map((obj: any) => + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + ); + if (isNotEmpty(otherLinks)) { + return observableOf(this.linkService.resolveLinks(paginatedList, ...otherLinks)); + } + } + } + return observableOf(paginatedList as any); + } + + /** + * Creates a {@link RemoteData} object for a rest request and its response + * + * @param requestUUID$ The UUID of the request we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildSingle(href$: string | Observable, ...linksToFollow: Array>): Observable> { + buildFromRequestUUID(requestUUID$: string | Observable, ...linksToFollow: Array>): Observable> { + if (typeof requestUUID$ === 'string') { + requestUUID$ = observableOf(requestUUID$); + } + const requestEntry$ = requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)); + + const payload$ = this.buildPayload(requestEntry$, undefined, ...linksToFollow); + + return this.toRemoteDataObservable(requestEntry$, payload$); + } + + /** + * Creates a {@link RemoteData} object for a rest request and its response + * + * @param href$ self link of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + buildFromHref(href$: string | Observable, ...linksToFollow: Array>): Observable> { if (typeof href$ === 'string') { href$ = observableOf(href$); } + + href$ = href$.pipe(map((href: string) => getUrlWithoutEmbedParams(href))); + const requestUUID$ = href$.pipe( switchMap((href: string) => this.objectCache.getRequestUUIDBySelfLink(href)), @@ -41,205 +213,143 @@ export class RemoteDataBuildService { const requestEntry$ = observableRace( href$.pipe(getRequestFromRequestHref(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), + ).pipe( + distinctUntilKeyChanged('lastUpdated') ); - // always use self link if that is cached, only if it isn't, get it via the response. - const payload$ = - observableCombineLatest( - href$.pipe( - switchMap((href: string) => this.objectCache.getObjectBySelfLink(href)), - startWith(undefined)), - requestEntry$.pipe( - getResourceLinksFromResponse(), - switchMap((resourceSelfLinks: string[]) => { - if (isNotEmpty(resourceSelfLinks)) { - return this.objectCache.getObjectBySelfLink(resourceSelfLinks[0]); - } else { - return observableOf(undefined); - } - }), - distinctUntilChanged(), - startWith(undefined) - ) - ).pipe( - map(([fromSelfLink, fromResponse]) => { - if (hasValue(fromSelfLink)) { - return fromSelfLink; - } else { - return fromResponse; - } - }), - hasValueOperator(), - map((obj: T) => - this.linkService.resolveLinks(obj, ...linksToFollow) - ), - startWith(undefined), - distinctUntilChanged() - ); - return this.toRemoteDataObservable(requestEntry$, payload$); + + const payload$ = this.buildPayload(requestEntry$, href$, ...linksToFollow); + + return this.toRemoteDataObservable(requestEntry$, payload$); + } + + /** + * Creates a single {@link RemoteData} object based on the response of a request to the REST server, with a list of + * {@link FollowLinkConfig} that indicate which embedded info should be added to the object + * @param href$ Observable href of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + buildSingle(href$: string | Observable, ...linksToFollow: Array>): Observable> { + return this.buildFromHref(href$, ...linksToFollow); } toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { - return observableCombineLatest(requestEntry$, payload$).pipe( - map(([reqEntry, payload]) => { - const requestPending = hasValue(reqEntry) && hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; - const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; - let isSuccessful: boolean; - let error: RemoteDataError; - const response = reqEntry ? reqEntry.response : undefined; - if (hasValue(response)) { - isSuccessful = response.statusCode === 204 || - response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload); - const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined; - if (hasValue(errorMessage)) { - error = new RemoteDataError( - response.statusCode, - response.statusText, - errorMessage - ); - } + return observableCombineLatest([ + requestEntry$, + payload$ + ]).pipe( + filter(([entry,payload]: [RequestEntry, T]) => + hasValue(entry) && + // filter out cases where the state is successful, but the payload isn't yet set + !(hasSucceeded(entry.state) && isUndefined(payload)) + ), + map(([entry, payload]: [RequestEntry, T]) => { + let response = entry.response; + if (hasNoValue(response)) { + response = {} as ResponseState; } - return new RemoteData( - requestPending, - responsePending, - isSuccessful, - error, - payload, - hasValue(response) ? response.statusCode : undefined - ); + return new RemoteData( + response.timeCompleted, + entry.request.responseMsToLive, + entry.lastUpdated, + entry.state, + response.errorMessage, + payload, + response.statusCode + ) }) ); } /** * Creates a list of {@link RemoteData} objects based on the response of a request to the REST server, with a list of + * + * Note: T extends HALResource not CacheableObject, because a PaginatedList is a CacheableObject in and of itself + * * {@link FollowLinkConfig} that indicate which embedded info should be added to the objects * @param href$ Observable href of objects we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildList(href$: string | Observable, ...linksToFollow: Array>): Observable>> { - if (typeof href$ === 'string') { - href$ = observableOf(href$); - } - - const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); - const tDomainList$ = requestEntry$.pipe( - getResourceLinksFromResponse(), - switchMap((resourceUUIDs: string[]) => { - return this.objectCache.getList(resourceUUIDs).pipe( - map((objs: T[]) => { - return objs.map((obj: T) => - this.linkService.resolveLinks(obj, ...linksToFollow) - ); - })); - }), - startWith([]), - distinctUntilChanged(), - ); - const pageInfo$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: DSOSuccessResponse) => { - if (hasValue(response.pageInfo)) { - return Object.assign(new PageInfo(), response.pageInfo); - } - }) - ); - - const payload$ = observableCombineLatest([tDomainList$, pageInfo$]).pipe( - map(([tDomainList, pageInfo]) => { - return new PaginatedList(pageInfo, tDomainList); - }) - ); - - return this.toRemoteDataObservable(requestEntry$, payload$); + buildList(href$: string | Observable, ...linksToFollow: Array>): Observable>> { + return this.buildFromHref>(href$, followLink('page', undefined, false, ...linksToFollow)); } + /** + * Turns an array of RemoteData observables in to an observable RemoteData[] + * + * By doing this you lose most of the info about the status of the original + * RemoteData objects, as you have to squash them down in to one. So use + * this only if the list you need isn't available on the REST API. If you + * need to use it, it's likely an indication that a REST endpoint is missing + * + * @param input the array of RemoteData observables to start from + */ aggregate(input: Array>>): Observable> { if (isEmpty(input)) { - return createSuccessfulRemoteDataObject$([]); + return createSuccessfulRemoteDataObject$([], new Date().getTime()); } - return observableCombineLatest(...input).pipe( + return observableCombineLatest(input).pipe( map((arr) => { - // The request of an aggregate RD should be pending if at least one - // of the RDs it's based on is still in the state RequestPending - const requestPending: boolean = arr - .map((d: RemoteData) => d.isRequestPending) - .find((b: boolean) => b === true); + const timeCompleted = arr + .map((d: RemoteData) => d.timeCompleted) + .reduce((max: number, current: number) => current > max ? current : max) - // The response of an aggregate RD should be pending if no requests - // are still pending and at least one of the RDs it's based - // on is still in the state ResponsePending - const responsePending: boolean = !requestPending && arr - .map((d: RemoteData) => d.isResponsePending) - .find((b: boolean) => b === true); + const msToLive = arr + .map((d: RemoteData) => d.msToLive) + .reduce((min: number, current: number) => current < min ? current : min) - let isSuccessful: boolean; - // isSuccessful should be undefined until all responses have come in. - // We can't know its state beforehand. We also can't say it's false - // because that would imply a request failed. - if (!(requestPending || responsePending)) { - isSuccessful = arr - .map((d: RemoteData) => d.hasSucceeded) - .every((b: boolean) => b === true); + const lastUpdated = arr + .map((d: RemoteData) => d.lastUpdated) + .reduce((max: number, current: number) => current > max ? current : max) + + let state: RequestEntryState; + if (arr.some((d: RemoteData) => d.isRequestPending)) { + state = RequestEntryState.RequestPending; + } else if (arr.some((d: RemoteData) => d.isResponsePending)) { + state = RequestEntryState.ResponsePending; + } else if (arr.some((d: RemoteData) => d.isErrorStale)) { + state = RequestEntryState.ErrorStale; + } else if (arr.some((d: RemoteData) => d.isError)) { + state = RequestEntryState.Error; + } else if (arr.some((d: RemoteData) => d.isSuccessStale)) { + state = RequestEntryState.SuccessStale; + } else { + state = RequestEntryState.Success; } const errorMessage: string = arr - .map((d: RemoteData) => d.error) - .map((e: RemoteDataError, idx: number) => { + .map((d: RemoteData) => d.errorMessage) + .map((e: string, idx: number) => { if (hasValue(e)) { - return `[${idx}]: ${e.message}`; + return `[${idx}]: ${e}`; } }).filter((e: string) => hasValue(e)) .join(', '); - const statusText: string = arr - .map((d: RemoteData) => d.error) - .map((e: RemoteDataError, idx: number) => { - if (hasValue(e)) { - return `[${idx}]: ${e.statusText}`; - } - }).filter((c: string) => hasValue(c)) - .join(', '); + const statusCodes = new Set(arr + .map((d: RemoteData) => d.statusCode)); - const statusCode: number = arr - .map((d: RemoteData) => d.error) - .map((e: RemoteDataError, idx: number) => { - if (hasValue(e)) { - return e.statusCode; - } - }).filter((c: number) => hasValue(c)) - .reduce((acc, status) => status, undefined); + let statusCode: number; - const error = new RemoteDataError(statusCode, statusText, errorMessage); + if (statusCodes.size === 1) { + statusCode = statusCodes.values().next().value; + } else if (statusCodes.size > 1) { + statusCode = 207; + } const payload: T[] = arr.map((d: RemoteData) => d.payload); return new RemoteData( - requestPending, - responsePending, - isSuccessful, - error, - payload + timeCompleted, + msToLive, + lastUpdated, + state, + errorMessage, + payload, + statusCode ); })) } - - private toPaginatedList(input: Observable>>, pageInfo: PageInfo): Observable>> { - return input.pipe( - map((rd: RemoteData>) => { - const rdAny = rd as any; - const newRD = new RemoteData(rdAny.requestPending, rdAny.responsePending, rdAny.isSuccessful, rd.error, undefined); - if (Array.isArray(rd.payload)) { - return Object.assign(newRD, { payload: new PaginatedList(pageInfo, rd.payload) }) - } else if (isNotUndefined(rd.payload)) { - return Object.assign(newRD, { payload: new PaginatedList(pageInfo, rd.payload.page) }); - } else { - return Object.assign(newRD, { payload: new PaginatedList(pageInfo, []) }); - } - }) - ); - } - } diff --git a/src/app/core/cache/cache-entry.ts b/src/app/core/cache/cache-entry.ts index 52ec7b8af0..ae302dfaeb 100644 --- a/src/app/core/cache/cache-entry.ts +++ b/src/app/core/cache/cache-entry.ts @@ -1,4 +1,4 @@ export interface CacheEntry { - timeAdded: number; + timeCompleted: number; msToLive: number; } diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 8531677ffc..ed509341a7 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -23,9 +23,10 @@ export class AddToObjectCacheAction implements Action { type = ObjectCacheActionTypes.ADD; payload: { objectToCache: CacheableObject; - timeAdded: number; + timeCompleted: number; msToLive: number; requestUUID: string; + alternativeLink?: string; }; /** @@ -33,17 +34,18 @@ export class AddToObjectCacheAction implements Action { * * @param objectToCache * the object to add - * @param timeAdded + * @param timeCompleted * the time it was added * @param msToLive * the amount of milliseconds before it should expire - * @param requestHref - * The href of the request that resulted in this object + * @param requestUUID + * The uuid of the request that resulted in this object * This isn't necessarily the same as the object's self * link, it could have been part of a list for example + * @param alternativeLink An optional alternative link to this object */ - constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestUUID: string) { - this.payload = { objectToCache, timeAdded, msToLive, requestUUID }; + constructor(objectToCache: CacheableObject, timeCompleted: number, msToLive: number, requestUUID: string, alternativeLink?: string) { + this.payload = { objectToCache, timeCompleted, msToLive, requestUUID, alternativeLink }; } } @@ -66,7 +68,7 @@ export class RemoveFromObjectCacheAction implements Action { } /** - * An ngrx action to reset the timeAdded property of all cached objects + * An ngrx action to reset the timeCompleted property of all cached objects */ export class ResetObjectCacheTimestampsAction implements Action { type = ObjectCacheActionTypes.RESET_TIMESTAMPS; @@ -76,7 +78,7 @@ export class ResetObjectCacheTimestampsAction implements Action { * Create a new ResetObjectCacheTimestampsAction * * @param newTimestamp - * the new timeAdded all objects should get + * the new timeCompleted all objects should get */ constructor(newTimestamp: number) { this.payload = newTimestamp; diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 6519e887c9..077a1e67f8 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -26,15 +26,20 @@ describe('objectCacheReducer', () => { const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3'; const newName = 'new different name'; + const altLink1 = 'https://alternative.link/endpoint/1234'; + const altLink2 = 'https://alternative.link/endpoint/5678'; + const altLink3 = 'https://alternative.link/endpoint/9123'; + const altLink4 = 'https://alternative.link/endpoint/4567'; const testState = { [selfLink1]: { data: { type: Item.type, self: selfLink1, foo: 'bar', - _links: { self: { href: selfLink1 } } + _links: { self: { href: selfLink1 } }, }, - timeAdded: new Date().getTime(), + alternativeLinks: [altLink1, altLink2], + timeCompleted: new Date().getTime(), msToLive: 900000, requestUUID: requestUUID1, patches: [], @@ -47,7 +52,8 @@ describe('objectCacheReducer', () => { foo: 'baz', _links: { self: { href: requestUUID2 } } }, - timeAdded: new Date().getTime(), + alternativeLinks: [altLink3, altLink4], + timeCompleted: new Date().getTime(), msToLive: 900000, requestUUID: selfLink2, patches: [], @@ -73,15 +79,16 @@ describe('objectCacheReducer', () => { it('should add the payload to the cache in response to an ADD action', () => { const state = Object.create(null); const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } }; - const timeAdded = new Date().getTime(); + const timeCompleted = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); + const action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, altLink1); const newState = objectCacheReducer(state, action); expect(newState[selfLink1].data).toEqual(objectToCache); - expect(newState[selfLink1].timeAdded).toEqual(timeAdded); + expect(newState[selfLink1].timeCompleted).toEqual(timeCompleted); expect(newState[selfLink1].msToLive).toEqual(msToLive); + expect(newState[selfLink1].alternativeLinks.includes(altLink1)).toBeTrue(); }); it('should overwrite an object in the cache in response to an ADD action if it already exists', () => { @@ -90,12 +97,12 @@ describe('objectCacheReducer', () => { foo: 'baz', somethingElse: true, type: Item.type, - _links: { self: { href: selfLink1 } } + _links: { self: { href: selfLink1 } }, }; - const timeAdded = new Date().getTime(); + const timeCompleted = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); + const action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, altLink1); const newState = objectCacheReducer(testState, action); /* tslint:disable:no-string-literal */ @@ -107,10 +114,10 @@ describe('objectCacheReducer', () => { it('should perform the ADD action without affecting the previous state', () => { const state = Object.create(null); const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } }; - const timeAdded = new Date().getTime(); + const timeCompleted = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); + const action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, altLink1); deepFreeze(state); objectCacheReducer(state, action); @@ -144,7 +151,7 @@ describe('objectCacheReducer', () => { const action = new ResetObjectCacheTimestampsAction(newTimestamp); const newState = objectCacheReducer(testState, action); Object.keys(newState).forEach((key) => { - expect(newState[key].timeAdded).toEqual(newTimestamp); + expect(newState[key].timeCompleted).toEqual(newTimestamp); }); }); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 930ed4f3f9..ce443cb931 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -6,7 +6,8 @@ import { AddToObjectCacheAction, RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, - AddPatchObjectCacheAction, ApplyPatchObjectCacheAction + AddPatchObjectCacheAction, + ApplyPatchObjectCacheAction } from './object-cache.actions'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { CacheEntry } from './cache-entry'; @@ -57,7 +58,6 @@ export const getResourceTypeValueFor = (type: any): string => { export class CacheableObject extends TypedObject implements HALResource { uuid?: string; handle?: string; - _links: { self: HALLink; } @@ -72,12 +72,42 @@ export class CacheableObject extends TypedObject implements HALResource { * An entry in the ObjectCache */ export class ObjectCacheEntry implements CacheEntry { + /** + * The object being cached + */ data: CacheableObject; - timeAdded: number; + + /** + * The timestamp for when this entry was set to completed + */ + timeCompleted: number; + + /** + * The number of milliseconds after the entry completes until it becomes stale + */ msToLive: number; + + /** + * The UUID of the request that caused this entry to be added + */ requestUUID: string; + + /** + * An array of patches that were made on the client side to this entry, but haven't been sent to the server yet + */ patches: Patch[] = []; + + /** + * Whether this entry has changes that haven't been sent to the server yet + */ isDirty: boolean; + + /** + * A list of links, apart from the self link, that also uniquely identify this object + * e.g. https://rest.api/collections/12345/logo could be an alternative link for a + * bitstream + */ + alternativeLinks: string[]; } /* tslint:enable:max-classes-per-file */ @@ -145,15 +175,17 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href]; + const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; return Object.assign({}, state, { [action.payload.objectToCache._links.self.href]: { data: action.payload.objectToCache, - timeAdded: action.payload.timeAdded, + timeCompleted: action.payload.timeCompleted, msToLive: action.payload.msToLive, requestUUID: action.payload.requestUUID, - isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false), - patches: (hasValue(existing) ? existing.patches : []) + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] } }); } @@ -180,20 +212,20 @@ function removeFromObjectCache(state: ObjectCacheState, action: RemoveFromObject } /** - * Set the timeAdded timestamp of every cached object to the specified value + * Set the timeCompleted timestamp of every cached object to the specified value * * @param state * the current state * @param action * a ResetObjectCacheTimestampsAction * @return ObjectCacheState - * the new state, with all timeAdded timestamps set to the specified value + * the new state, with all timeCompleted timestamps set to the specified value */ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObjectCacheTimestampsAction): ObjectCacheState { const newState = Object.create(null); Object.keys(state).forEach((key) => { newState[key] = Object.assign({}, state[key], { - timeAdded: action.payload + timeCompleted: action.payload }); }); return newState; diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index e7c208e095..060f4d0475 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -1,54 +1,73 @@ import * as ngrx from '@ngrx/store'; import { Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch'; -import { of as observableOf } from 'rxjs'; +import { empty, of as observableOf } from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreState } from '../core.reducers'; import { RestRequestMethod } from '../data/rest-request-method'; import { Item } from '../shared/item.model'; -import { - AddPatchObjectCacheAction, - AddToObjectCacheAction, - ApplyPatchObjectCacheAction, - RemoveFromObjectCacheAction -} from './object-cache.actions'; +import { AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { Patch } from './object-cache.reducer'; - import { ObjectCacheService } from './object-cache.service'; import { AddToSSBAction } from './server-sync-buffer.actions'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { IndexName } from '../index/index.reducer'; +import { HALLink } from '../shared/hal-link.model'; +import { getTestScheduler } from 'jasmine-marbles'; describe('ObjectCacheService', () => { let service: ObjectCacheService; let store: Store; let linkServiceStub; - const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; - const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736'; - const timestamp = new Date().getTime(); - const msToLive = 900000; - let objectToCache = { - type: Item.type, - _links: { - self: { href: selfLink } - } - }; + let selfLink; + let anotherLink; + let altLink1; + let altLink2; + let requestUUID; + let alternativeLink; + let timestamp; + let timestamp2; + let msToLive; + let msToLive2 + let objectToCache; let cacheEntry; + let cacheEntry2; let invalidCacheEntry; - const operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; + let operations; function init() { + selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + anotherLink = 'https://another.link/endpoint/1234'; + altLink1 = 'https://alternative.link/endpoint/1234'; + altLink2 = 'https://alternative.link/endpoint/5678'; + requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736'; + alternativeLink = 'https://rest.api/endpoint/5e4f8a5-be98-4c51-9fd8-6bfedcbd59b7/item'; + timestamp = new Date().getTime(); + timestamp2 = new Date().getTime() - 200; + msToLive = 900000; + msToLive2 = 120000; objectToCache = { type: Item.type, _links: { - self: { href: selfLink } + self: { href: selfLink }, + anotherLink: { href: anotherLink } } }; cacheEntry = { data: objectToCache, - timeAdded: timestamp, - msToLive: msToLive + timeCompleted: timestamp, + msToLive: msToLive, + alternativeLinks: [altLink1, altLink2] }; - invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 }) + cacheEntry2 = { + data: objectToCache, + timeCompleted: timestamp2, + msToLive: msToLive2, + alternativeLinks: [altLink2] + }; + invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 }); + operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; } beforeEach(() => { @@ -68,47 +87,73 @@ describe('ObjectCacheService', () => { describe('add', () => { it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => { - service.add(objectToCache, msToLive, requestUUID); - expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID)); + service.add(objectToCache, msToLive, requestUUID, alternativeLink); + expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID, alternativeLink)); expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalledWith(objectToCache); }); }); describe('remove', () => { + beforeEach(() => { + spyOn(service as any, 'getByHref').and.returnValue(observableOf(cacheEntry)); + }); + it('should dispatch a REMOVE action with the self link of the object to remove', () => { service.remove(selfLink); expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(selfLink)); }); - }); - describe('getBySelfLink', () => { - it('should return an observable of the cached object with the specified self link and type', () => { - spyOnProperty(ngrx, 'select').and.callFake(() => { - return () => { - return () => observableOf(cacheEntry); - }; - }); - - // due to the implementation of spyOn above, this subscribe will be synchronous - service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => { - expect(o._links.self.href).toBe(selfLink); - // this only works if testObj is an instance of TestClass - expect(o instanceof Item).toBeTruthy(); - } - ); + it('should dispatch a REMOVE_BY_SUBSTRING action on the index state for each alternativeLink in the object', () => { + service.remove(selfLink); + cacheEntry.alternativeLinks.forEach( + (link: string) => expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link))) }); - it('should not return a cached object that has exceeded its time to live', () => { - spyOnProperty(ngrx, 'select').and.callFake(() => { - return () => { - return () => observableOf(invalidCacheEntry); - }; + it('should dispatch a REMOVE_BY_SUBSTRING action on the index state for each _links in the object, except the self link', () => { + service.remove(selfLink); + Object.entries(objectToCache._links).forEach(([key, value]: [string, HALLink]) => { + if (key !== 'self') { + expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, value.href)) + } + }); + }); + }); + + describe('getByHref', () => { + describe('if getBySelfLink emits a valid object and getByAlternativeLink emits undefined', () => { + beforeEach(() => { + spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry)); + spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(undefined)); }); - let getObsHasFired = false; - const subscription = service.getObjectBySelfLink(selfLink).subscribe((o) => getObsHasFired = true); - expect(getObsHasFired).toBe(false); - subscription.unsubscribe(); + it('should return the object emitted by getBySelfLink', () => { + const result = service.getByHref(selfLink); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry }) + }); + }); + + describe('if getBySelfLink emits undefined and getByAlternativeLink a valid object', () => { + beforeEach(() => { + spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(undefined)); + spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(cacheEntry)); + }); + + it('should return the object emitted by getByAlternativeLink', () => { + const result = service.getByHref(selfLink); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry }) + }); + }); + + describe('if getBySelfLink emits an invalid and getByAlternativeLink a valid object', () => { + beforeEach(() => { + spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry)); + spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(cacheEntry2)); + }); + + it('should return the object emitted by getByAlternativeLink', () => { + const result = service.getByHref(selfLink); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry2 }) + }); }); }); @@ -117,7 +162,7 @@ describe('ObjectCacheService', () => { const item = Object.assign(new Item(), { _links: { self: { href: selfLink } } }); - spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item)); + spyOn(service, 'getObjectByHref').and.returnValue(observableOf(item)); service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => { expect(arr[0]._links.self.href).toBe(selfLink); @@ -127,34 +172,52 @@ describe('ObjectCacheService', () => { }); describe('has', () => { - it('should return true if the object with the supplied self link is cached and still valid', () => { + + describe('getByHref emits an object', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(observableOf(cacheEntry)) + }); + + it('should return true', () => { + expect(service.hasByHref(selfLink)).toBe(true); + }); + }); + + describe('getByHref emits nothing', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(empty()) + }); + + it('should return false', () => { + expect(service.hasByHref(selfLink)).toBe(false); + }); + }) + }); + + describe('getBySelfLink', () => { + it('should return the entry returned by the select method', () => { spyOnProperty(ngrx, 'select').and.callFake(() => { return () => { return () => observableOf(cacheEntry); }; }); - expect(service.hasBySelfLink(selfLink)).toBe(true); + getTestScheduler().expectObservable((service as any).getBySelfLink(selfLink)).toBe('(a|)', { a: cacheEntry }); }); + }); - it('should return false if the object with the supplied self link isn\'t cached', () => { + describe('getByAlternativeLink', () => { + beforeEach(() => { + spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry)); + }); + it('should call getBySelfLink with the value returned by the select method', () => { spyOnProperty(ngrx, 'select').and.callFake(() => { return () => { - return () => observableOf(undefined); + return () => observableOf(anotherLink); }; }); - - expect(service.hasBySelfLink(selfLink)).toBe(false); - }); - - it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => { - spyOnProperty(ngrx, 'select').and.callFake(() => { - return () => { - return () => observableOf(invalidCacheEntry); - }; - }); - - expect(service.hasBySelfLink(selfLink)).toBe(false); + (service as any).getByAlternativeLink(selfLink).subscribe(); + expect((service as any).getBySelfLink).toHaveBeenCalledWith(anotherLink); }); }); @@ -169,7 +232,8 @@ describe('ObjectCacheService', () => { cacheEntry.patches = [ { operations: operations - } as Patch]; + } as Patch + ]; const result = (service as any).isDirty(cacheEntry); expect(result).toBe(true); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 745133373d..2bedc44b47 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,14 +1,17 @@ import { Injectable } from '@angular/core'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { applyPatch, Operation } from 'fast-json-patch'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; -import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; +import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; -import { selfLinkFromUuidSelector } from '../index/index.selectors'; +import { + selfLinkFromAlternativeLinkSelector, + selfLinkFromUuidSelector +} from '../index/index.selectors'; import { GenericConstructor } from '../shared/generic-constructor'; import { getClassForType } from './builders/build-decorators'; import { LinkService } from './builders/link.service'; @@ -21,6 +24,9 @@ import { import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { AddToSSBAction } from './server-sync-buffer.actions'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { IndexName } from '../index/index.reducer'; +import { HALLink } from '../shared/hal-link.model'; /** * The base selector function to select the object cache in the store @@ -60,10 +66,12 @@ export class ObjectCacheService { * The number of milliseconds it should be cached for * @param requestUUID * The UUID of the request that resulted in this object + * @param alternativeLink + * An optional alternative link to this object */ - add(object: CacheableObject, msToLive: number, requestUUID: string): void { + add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links - this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID)); + this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } /** @@ -73,9 +81,33 @@ export class ObjectCacheService { * The unique href of the object to be removed */ remove(href: string): void { + this.removeRelatedLinksFromIndex(href); this.store.dispatch(new RemoveFromObjectCacheAction(href)); } + private removeRelatedLinksFromIndex(href: string) { + const cacheEntry$ = this.getByHref(href); + const altLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => entry.alternativeLinks), take(1)); + const childLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => { + return Object + .entries(entry.data._links) + .filter(([key, value]: [string, HALLink]) => key !== 'self') + .map(([key, value]: [string, HALLink]) => value.href); + }), + take(1) + ); + this.removeLinksFromAlternativeLinkIndex(altLinks$); + this.removeLinksFromAlternativeLinkIndex(childLinks$); + + } + + private removeLinksFromAlternativeLinkIndex(links$: Observable) { + links$.subscribe((links: string[]) => links.forEach((link: string) => { + this.store.dispatch(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)); + } + )) + } + /** * Get an observable of the object with the specified UUID * @@ -88,7 +120,7 @@ export class ObjectCacheService { Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) + mergeMap((selfLink: string) => this.getObjectByHref(selfLink) ) ) } @@ -96,13 +128,13 @@ export class ObjectCacheService { /** * Get an observable of the object with the specified selfLink * - * @param selfLink - * The selfLink of the object to get + * @param href + * The href of the object to get * @return Observable * An observable of the requested object */ - getObjectBySelfLink(selfLink: string): Observable { - return this.getBySelfLink(selfLink).pipe( + getObjectByHref(href: string): Observable { + return this.getByHref(href).pipe( map((entry: ObjectCacheEntry) => { if (isNotEmpty(entry.patches)) { const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); @@ -126,19 +158,34 @@ export class ObjectCacheService { /** * Get an observable of the object cache entry with the specified selfLink * - * @param selfLink - * The selfLink of the object to get + * @param href + * The href of the object to get * @return Observable * An observable of the requested object cache entry */ - getBySelfLink(selfLink: string): Observable { + getByHref(href: string): Observable { + return observableCombineLatest([ + this.getByAlternativeLink(href), + this.getBySelfLink(href), + ]).pipe( + map((results: ObjectCacheEntry[]) => results.find((entry: ObjectCacheEntry) => hasValue(entry))), + filter((entry: ObjectCacheEntry) => hasValue(entry)) + ); + } + + private getBySelfLink(selfLink: string): Observable { return this.store.pipe( select(entryFromSelfLinkSelector(selfLink)), - filter((entry) => this.isValid(entry)), - distinctUntilChanged() ); } + private getByAlternativeLink(alternativeLink: string): Observable { + return this.store.pipe( + select(selfLinkFromAlternativeLinkSelector(alternativeLink)), + switchMap((selfLink) => this.getBySelfLink(selfLink)), + ) + } + /** * Get an observable of the request's uuid with the specified selfLink * @@ -148,7 +195,7 @@ export class ObjectCacheService { * An observable of the request's uuid */ getRequestUUIDBySelfLink(selfLink: string): Observable { - return this.getBySelfLink(selfLink).pipe( + return this.getByHref(selfLink).pipe( map((entry: ObjectCacheEntry) => entry.requestUUID), distinctUntilChanged()); } @@ -188,9 +235,13 @@ export class ObjectCacheService { * @return Observable> */ getList(selfLinks: string[]): Observable { - return observableCombineLatest( - selfLinks.map((selfLink: string) => this.getObjectBySelfLink(selfLink)) - ); + if (isEmpty(selfLinks)) { + return observableOf([]); + } else { + return observableCombineLatest( + selfLinks.map((selfLink: string) => this.getObjectByHref(selfLink)) + ); + } } /** @@ -209,64 +260,49 @@ export class ObjectCacheService { this.store.pipe( select(selfLinkFromUuidSelector(uuid)), take(1) - ).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); + ).subscribe((selfLink: string) => result = this.hasByHref(selfLink)); return result; } /** - * Check whether the object with the specified self link is cached + * Check whether the object with the specified self link is cached. Note that it doesn't check + * whether the response this object came in on is still valid. Just whether it exists. * - * @param selfLink - * The self link of the object to check + * @param href + * The href of the object to check + * @param requestUUID + * Optional. If the object exists, check whether it links back to this requestUUID * @return boolean - * true if the object with the specified self link is cached, + * true if the object with the specified href is cached, * false otherwise */ - hasBySelfLink(selfLink: string): boolean { + hasByHref(href: string, requestUUID?: string): boolean { let result = false; - - this.store.pipe(select(entryFromSelfLinkSelector(selfLink)), - take(1) - ).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry)); - + this.getByHref(href).subscribe((entry: ObjectCacheEntry) => { + if (isNotEmpty(requestUUID)) { + result = entry.requestUUID === requestUUID; + } else { + result = true; + } + }).unsubscribe(); return result; } /** * Create an observable that emits a new value whenever the availability of the cached object changes. * The value it emits is a boolean stating if the object exists in cache or not. - * @param selfLink The self link of the object to observe + * @param href The self link of the object to observe */ - hasBySelfLinkObservable(selfLink: string): Observable { - return this.store.pipe( - select(entryFromSelfLinkSelector(selfLink)), - map((entry: ObjectCacheEntry) => this.isValid(entry)) + hasByHref$(href: string): Observable { + return observableCombineLatest( + this.getBySelfLink(href), + this.getByAlternativeLink(href) + ).pipe( + map((entries: ObjectCacheEntry[]) => entries.some((entry) => hasValue(entry))) ); } - /** - * Check whether an ObjectCacheEntry should still be cached - * - * @param entry - * the entry to check - * @return boolean - * false if the entry is null, undefined, or its time to - * live has been exceeded, true otherwise - */ - private isValid(entry: ObjectCacheEntry): boolean { - if (hasNoValue(entry)) { - return false; - } else { - const timeOutdated = entry.timeAdded + entry.msToLive; - const isOutDated = new Date().getTime() > timeOutdated; - if (isOutDated) { - this.store.dispatch(new RemoveFromObjectCacheAction(entry.data._links.self.href)); - } - return !isOutDated; - } - } - /** * Add operations to the existing list of operations for an ObjectCacheEntry * Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index b33080b641..3c7c272830 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -1,22 +1,14 @@ -import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; -import { AuthStatus } from '../auth/models/auth-status.model'; import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; import { ConfigObject } from '../config/models/config.model'; -import { FacetValue } from '../../shared/search/facet-value.model'; -import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; -import { PaginatedList } from '../data/paginated-list'; -import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { MetadataField } from '../metadata/metadata-field.model'; -import { ContentSource } from '../shared/content-source.model'; -import { Registration } from '../shared/registration.model'; +import { HALLink } from '../shared/hal-link.model'; +import { UnCacheableObject } from '../shared/uncacheable-object.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { public toCache = true; - public timeAdded: number; + public timeCompleted: number; constructor( public isSuccessful: boolean, @@ -26,6 +18,12 @@ export class RestResponse { } } +export class ParsedResponse extends RestResponse { + constructor(statusCode: number, public link?: HALLink, public unCacheableObject?: UnCacheableObject) { + super(true, statusCode, `${statusCode}`); + } +} + export class DSOSuccessResponse extends RestResponse { constructor( public resourceSelfLinks: string[], @@ -37,79 +35,8 @@ export class DSOSuccessResponse extends RestResponse { } } -/** - * A successful response containing exactly one MetadataSchema - */ -export class MetadataschemaSuccessResponse extends RestResponse { - constructor( - public metadataschema: MetadataSchema, - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing exactly one MetadataField - */ -export class MetadatafieldSuccessResponse extends RestResponse { - constructor( - public metadatafield: MetadataField, - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} - -export class SearchSuccessResponse extends RestResponse { - constructor( - public results: SearchQueryResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - -export class FacetConfigSuccessResponse extends RestResponse { - constructor( - public results: SearchFilterConfig[], - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} - -export class FacetValueMap { - [name: string]: FacetValueSuccessResponse -} - -export class FacetValueSuccessResponse extends RestResponse { - constructor( - public results: FacetValue[], - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo) { - super(true, statusCode, statusText); - } -} - -export class FacetValueMapSuccessResponse extends RestResponse { - constructor( - public results: FacetValueMap, - public statusCode: number, - public statusText: string - ) { - super(true, statusCode, statusText); - } -} - export class EndpointMap { - [linkPath: string]: string + [linkPath: string]: HALLink } export class EndpointMapSuccessResponse extends RestResponse { @@ -122,17 +49,6 @@ export class EndpointMapSuccessResponse extends RestResponse { } } -export class GenericSuccessResponse extends RestResponse { - constructor( - public payload: T, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - export class ErrorResponse extends RestResponse { errorMessage: string; @@ -154,18 +70,6 @@ export class ConfigSuccessResponse extends RestResponse { } } -export class AuthStatusResponse extends RestResponse { - public toCache = false; - - constructor( - public response: AuthStatus, - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} - /** * A REST Response containing a token */ @@ -191,17 +95,6 @@ export class PostPatchSuccessResponse extends RestResponse { } } -export class SubmissionSuccessResponse extends RestResponse { - constructor( - public dataDefinition: Array, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - export class EpersonSuccessResponse extends RestResponse { constructor( public epersonDefinition: DSpaceObject[], @@ -247,30 +140,4 @@ export class FilteredDiscoveryQueryResponse extends RestResponse { super(true, statusCode, statusText); } } - -/** - * A successful response containing exactly one MetadataSchema - */ -export class ContentSourceSuccessResponse extends RestResponse { - constructor( - public contentsource: ContentSource, - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing a Registration - */ -export class RegistrationSuccessResponse extends RestResponse { - constructor( - public registration: Registration, - public statusCode: number, - public statusText: string, - ) { - super(true, statusCode, statusText); - } -} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index 245b6f67d8..9d9d91d9a1 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -51,7 +51,7 @@ describe('ServerSyncBufferEffects', () => { }); return observableOf(object); }, - getBySelfLink: (link) => { + getByHref: (link) => { const object = Object.assign(new DSpaceObject(), { _links: { self: { href: link } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 7d57bb4433..7d8766b096 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -90,7 +90,7 @@ export class ServerSyncBufferEffects { * @returns {Observable} ApplyPatchObjectCacheAction to be dispatched */ private applyPatch(href: string): Observable { - const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1)); + const patchObject = this.objectCache.getByHref(href).pipe(take(1)); return patchObject.pipe( map((entry: ObjectCacheEntry) => { diff --git a/src/app/core/config/config-data.ts b/src/app/core/config/config-data.ts deleted file mode 100644 index cb40514e45..0000000000 --- a/src/app/core/config/config-data.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { ConfigObject } from './models/config.model'; - -/** - * A class to represent the data retrieved by a configuration service - */ -export class ConfigData { - constructor( - public pageInfo: PageInfo, - public payload: ConfigObject - ) { - } -} diff --git a/src/app/core/config/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts deleted file mode 100644 index 3328b48f04..0000000000 --- a/src/app/core/config/config-response-parsing.service.spec.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { Store } from '@ngrx/store'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models'; -import { CoreState } from '../core.reducers'; -import { PaginatedList } from '../data/paginated-list'; -import { ConfigRequest } from '../data/request.models'; -import { PageInfo } from '../shared/page-info.model'; -import { ConfigResponseParsingService } from './config-response-parsing.service'; -import { SubmissionDefinitionModel } from './models/config-submission-definition.model'; -import { SubmissionSectionModel } from './models/config-submission-section.model'; - -describe('ConfigResponseParsingService', () => { - let service: ConfigResponseParsingService; - - const store = {} as Store; - const objectCacheService = new ObjectCacheService(store, undefined); - let validResponse; - beforeEach(() => { - service = new ConfigResponseParsingService(objectCacheService); - validResponse = { - payload: { - id: 'traditional', - name: 'traditional', - type: 'submissiondefinition', - isDefault: true, - _links: { - sections: { - href: 'https://rest.api/config/submissiondefinitions/traditional/sections' - }, - self: { - href: 'https://rest.api/config/submissiondefinitions/traditional' - } - }, - _embedded: { - sections: { - page: { - number: 0, - size: 4, - totalPages: 1, totalElements: 4 - }, - _embedded: [ - { - id: 'traditionalpageone', header: 'submit.progressbar.describe.stepone', - mandatory: true, - sectionType: 'submission-form', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { - href: 'https://rest.api/config/submissionsections/traditionalpageone' - }, - config: { - href: 'https://rest.api/config/submissionforms/traditionalpageone' - } - } - }, { - id: 'traditionalpagetwo', - header: 'submit.progressbar.describe.steptwo', - mandatory: true, - sectionType: 'submission-form', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { - href: 'https://rest.api/config/submissionsections/traditionalpagetwo' - }, - config: { - href: 'https://rest.api/config/submissionforms/traditionalpagetwo' - } - } - }, { - id: 'upload', - header: 'submit.progressbar.upload', - mandatory: false, - sectionType: 'upload', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { - href: 'https://rest.api/config/submissionsections/upload' - }, - config: { - href: 'https://rest.api/config/submissionuploads/upload' - } - } - }, { - id: 'license', - header: 'submit.progressbar.license', - mandatory: true, - sectionType: 'license', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { - href: 'https://rest.api/config/submissionsections/license' - } - } - } - ], - _links: { - self: { - href: 'https://rest.api/config/submissiondefinitions/traditional/sections' - } - } - } - } - }, - statusCode: 200, - statusText: 'OK' - }; - }); - - describe('parse', () => { - const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional'); - - const invalidResponse1 = { - payload: {}, - statusCode: 200, - statusText: 'OK' - }; - - const invalidResponse2 = { - payload: { - id: 'traditional', - name: 'traditional', - type: 'submissiondefinition', - isDefault: true, - _links: {}, - _embedded: { - sections: { - page: { - number: 0, - size: 4, - totalPages: 1, totalElements: 4 - }, - _embedded: [{}, {}], - _links: { - self: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' } - } - } - } - }, - statusCode: 200, - statusText: 'OK' - }; - - const invalidResponse3 = { - payload: { - _links: { self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: 500, statusText: 'Internal Server Error' - }; - const pageinfo = Object.assign(new PageInfo(), { - elementsPerPage: 4, - totalElements: 4, - totalPages: 1, - currentPage: 1, - _links: { - self: { - href: 'https://rest.api/config/submissiondefinitions/traditional/sections' - }, - }, - }); - const definitions = - Object.assign(new SubmissionDefinitionModel(), { - isDefault: true, - name: 'traditional', - id: 'traditional', - type: 'submissiondefinition', - _links: { - sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' }, - self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } - }, - sections: new PaginatedList(pageinfo, [ - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.describe.stepone', - mandatory: true, - sectionType: 'submission-form', - id: 'traditionalpageone', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { href: 'https://rest.api/config/submissionsections/traditionalpageone' }, - config: { href: 'https://rest.api/config/submissionforms/traditionalpageone' } - }, - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.describe.steptwo', - mandatory: true, - sectionType: 'submission-form', - id: 'traditionalpagetwo', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { href: 'https://rest.api/config/submissionsections/traditionalpagetwo' }, - config: { href: 'https://rest.api/config/submissionforms/traditionalpagetwo' } - }, - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.upload', - mandatory: false, - sectionType: 'upload', - id: 'upload', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { href: 'https://rest.api/config/submissionsections/upload' }, - config: { href: 'https://rest.api/config/submissionuploads/upload' } - }, - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.license', - mandatory: true, - sectionType: 'license', - id: 'license', - visibility: { - main: null, - other: 'READONLY' - }, - type: 'submissionsection', - _links: { - self: { href: 'https://rest.api/config/submissionsections/license' } - }, - }) - ]) - }); - - it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { - const response = service.parse(validRequest, validResponse); - expect(response.constructor).toBe(ConfigSuccessResponse); - }); - - it('should return an ErrorResponse if data contains an invalid config endpoint response', () => { - const response1 = service.parse(validRequest, invalidResponse1); - const response2 = service.parse(validRequest, invalidResponse2); - expect(response1.constructor).toBe(ErrorResponse); - expect(response2.constructor).toBe(ErrorResponse); - }); - - it('should return an ErrorResponse if data contains a statuscode other than 200', () => { - const response = service.parse(validRequest, invalidResponse3); - expect(response.constructor).toBe(ErrorResponse); - }); - - it('should return a ConfigSuccessResponse with the ConfigDefinitions in data', () => { - const response = service.parse(validRequest, validResponse); - expect((response as any).configDefinition).toEqual(definitions); - }); - - }); -}); diff --git a/src/app/core/config/config-response-parsing.service.ts b/src/app/core/config/config-response-parsing.service.ts deleted file mode 100644 index a603de291a..0000000000 --- a/src/app/core/config/config-response-parsing.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; - -import { ResponseParsingService } from '../data/parsing.service'; -import { RestRequest } from '../data/request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; -import { isNotEmpty } from '../../shared/empty.util'; - -import { ConfigObject } from './models/config.model'; -import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; - -@Injectable() -export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected toCache = false; - protected shouldDirectlyAttachEmbeds = true; - - constructor( - protected objectCache: ObjectCacheService, - ) { super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 201 || data.statusCode === 200)) { - const configDefinition = this.process(data.payload, request); - return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from config endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); - } - } - -} diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 80febb711f..48027be1fc 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,11 +1,13 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; -import { ConfigRequest, FindListOptions } from '../data/request.models'; +import { FindListOptions, GetRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; @@ -16,8 +18,9 @@ class TestService extends ConfigService { constructor( protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService) { - super(); + super(requestService, rdbService, null, null, halService, null, null, null, BROWSE); } } @@ -25,6 +28,7 @@ describe('ConfigService', () => { let scheduler: TestScheduler; let service: TestService; let requestService: RequestService; + let rdbService: RemoteDataBuildService; let halService: any; const findOptions: FindListOptions = new FindListOptions(); @@ -39,6 +43,7 @@ describe('ConfigService', () => { function initTestService(): TestService { return new TestService( requestService, + rdbService, halService ); } @@ -46,38 +51,16 @@ describe('ConfigService', () => { beforeEach(() => { scheduler = getTestScheduler(); requestService = getMockRequestService(); + rdbService = getMockRemoteDataBuildService(); halService = new HALEndpointServiceStub(configEndpoint); service = initTestService(); }); - describe('getConfigByHref', () => { + describe('findByHref', () => { - it('should configure a new ConfigRequest', () => { - const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint); - scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - }); - - describe('getConfigByName', () => { - - it('should configure a new ConfigRequest', () => { - const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint); - scheduler.schedule(() => service.getConfigByName(scopeName).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - }); - - describe('getConfigBySearch', () => { - - it('should configure a new ConfigRequest', () => { - findOptions.scopeID = scopeID; - const expected = new ConfigRequest(requestService.generateRequestId(), searchEndpoint); - scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe()); + it('should configure a new GetRequest', () => { + const expected = new GetRequest(requestService.generateRequestId(), scopedEndpoint); + scheduler.schedule(() => service.findByHref(scopedEndpoint).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index db14c4a256..aa8592044a 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -1,107 +1,67 @@ -import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { RequestService } from '../data/request.service'; -import { ConfigSuccessResponse } from '../cache/response.models'; -import { ConfigRequest, FindListOptions, RestRequest } from '../data/request.models'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ConfigData } from './config-data'; -import { getResponseFromEntry } from '../shared/operators'; - -export abstract class ConfigService { - protected request: ConfigRequest; - protected abstract requestService: RequestService; - protected abstract linkPath: string; - protected abstract browseEndpoint: string; - protected abstract halService: HALEndpointService; - - protected getConfig(request: RestRequest): Observable { - const responses = this.requestService.getByHref(request.href).pipe( - getResponseFromEntry() - ); - const errorResponses = responses.pipe( - filter((response) => !response.isSuccessful), - mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`))) - ); - const successResponses = responses.pipe( - filter((response) => response.isSuccessful && isNotEmpty(response) && isNotEmpty((response as ConfigSuccessResponse).configDefinition)), - map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) - ); - return observableMerge(errorResponses, successResponses); +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ConfigObject } from './models/config.model'; +import { RemoteData } from '../data/remote-data'; +import { DataService } from '../data/data.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { map } from 'rxjs/operators'; +class DataServiceImpl extends DataService { + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer, + protected linkPath: string + ) { + super(); + } +} + +// tslint:disable-next-line:max-classes-per-file +export abstract class ConfigService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer, + protected linkPath: string + ) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, this.linkPath); + } + + public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasFailed) { + throw new Error(`Couldn't retrieve the config`); + } else { + return rd; + } + }) + ); } - - protected getConfigByNameHref(endpoint, resourceName): string { - return `${endpoint}/${resourceName}`; - } - - protected getConfigSearchHref(endpoint, options: FindListOptions = {}): string { - let result; - const args = []; - - if (hasValue(options.scopeID)) { - result = `${endpoint}/${this.browseEndpoint}`; - args.push(`uuid=${options.scopeID}`); - } else { - result = endpoint; - } - - 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.push(`page=${options.currentPage - 1}`); - } - - if (hasValue(options.elementsPerPage)) { - args.push(`size=${options.elementsPerPage}`); - } - - if (hasValue(options.sort)) { - args.push(`sort=${options.sort.field},${options.sort.direction}`); - } - - if (isNotEmpty(args)) { - result = `${result}?${args.join('&')}`; - } - return result; - } - - public getConfigAll(): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - filter((href: string) => isNotEmpty(href)), - distinctUntilChanged(), - map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: RestRequest) => this.requestService.configure(request)), - mergeMap((request: RestRequest) => this.getConfig(request)), - distinctUntilChanged()); - } - - public getConfigByHref(href: string): Observable { - const request = new ConfigRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); - - return this.getConfig(request); - } - - public getConfigByName(name: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getConfigByNameHref(endpoint, name)), - filter((href: string) => isNotEmpty(href)), - distinctUntilChanged(), - map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: RestRequest) => this.requestService.configure(request)), - mergeMap((request: RestRequest) => this.getConfig(request)), - distinctUntilChanged()); - } - - public getConfigBySearch(options: FindListOptions = {}): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getConfigSearchHref(endpoint, options)), - filter((href: string) => isNotEmpty(href)), - distinctUntilChanged(), - map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: RestRequest) => this.requestService.configure(request)), - mergeMap((request: RestRequest) => this.getConfig(request)), - distinctUntilChanged()); - } - } diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts index f3e888d513..b07917e032 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -1,10 +1,10 @@ import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; -import { PaginatedList } from '../../data/paginated-list'; +import { PaginatedList } from '../../data/paginated-list.model'; import { HALLink } from '../../shared/hal-link.model'; -import { ResourceType } from '../../shared/resource-type'; import { SubmissionSectionModel } from './config-submission-section.model'; import { ConfigObject } from './config.model'; +import { SUBMISSION_DEFINITION_TYPE } from './config-type'; /** * Class for the configuration describing the submission @@ -12,7 +12,7 @@ import { ConfigObject } from './config.model'; @typedObject @inheritSerialization(ConfigObject) export class SubmissionDefinitionModel extends ConfigObject { - static type = new ResourceType('submissiondefinition'); + static type = SUBMISSION_DEFINITION_TYPE; /** * A boolean representing if this submission definition is the default or not diff --git a/src/app/core/config/models/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts index 1fdf571806..08f1ef17bb 100644 --- a/src/app/core/config/models/config-submission-definitions.model.ts +++ b/src/app/core/config/models/config-submission-definitions.model.ts @@ -1,11 +1,11 @@ import { inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionDefinitionModel } from './config-submission-definition.model'; -import { ResourceType } from '../../shared/resource-type'; +import { SUBMISSION_DEFINITIONS_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionDefinitionModel) export class SubmissionDefinitionsModel extends SubmissionDefinitionModel { - static type = new ResourceType('submissiondefinitions'); + static type = SUBMISSION_DEFINITIONS_TYPE; } diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts index d3fcfa9738..90f94882bd 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -2,7 +2,7 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; import { ConfigObject } from './config.model'; import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; -import { ResourceType } from '../../shared/resource-type'; +import { SUBMISSION_FORM_TYPE } from './config-type'; /** * An interface that define a form row and its properties. @@ -17,7 +17,7 @@ export interface FormRowModel { @typedObject @inheritSerialization(ConfigObject) export class SubmissionFormModel extends ConfigObject { - static type = new ResourceType('submissionform'); + static type = SUBMISSION_FORM_TYPE; /** * An array of [FormRowModel] that are present in this form diff --git a/src/app/core/config/models/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts index 8130bf3264..506905d88c 100644 --- a/src/app/core/config/models/config-submission-forms.model.ts +++ b/src/app/core/config/models/config-submission-forms.model.ts @@ -1,7 +1,7 @@ import { inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionFormModel } from './config-submission-form.model'; -import { ResourceType } from '../../shared/resource-type'; +import { SUBMISSION_FORMS_TYPE } from './config-type'; /** * A model class for a NormalizedObject. @@ -9,5 +9,5 @@ import { ResourceType } from '../../shared/resource-type'; @typedObject @inheritSerialization(SubmissionFormModel) export class SubmissionFormsModel extends SubmissionFormModel { - static type = new ResourceType('submissionforms'); + static type = SUBMISSION_FORMS_TYPE; } diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index d8249297b1..cb89b3be94 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -2,8 +2,8 @@ import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { SectionsType } from '../../../submission/sections/sections-type'; import { typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; -import { ResourceType } from '../../shared/resource-type'; import { ConfigObject } from './config.model'; +import { SUBMISSION_SECTION_TYPE } from './config-type'; /** * An interface that define section visibility and its properties. @@ -16,7 +16,7 @@ export interface SubmissionSectionVisibility { @typedObject @inheritSerialization(ConfigObject) export class SubmissionSectionModel extends ConfigObject { - static type = new ResourceType('submissionsection'); + static type = SUBMISSION_SECTION_TYPE; /** * The header for this section diff --git a/src/app/core/config/models/config-submission-sections.model.ts b/src/app/core/config/models/config-submission-sections.model.ts index 7f78712273..423ea99b1e 100644 --- a/src/app/core/config/models/config-submission-sections.model.ts +++ b/src/app/core/config/models/config-submission-sections.model.ts @@ -1,10 +1,10 @@ import { inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionSectionModel } from './config-submission-section.model'; -import { ResourceType } from '../../shared/resource-type'; +import { SUBMISSION_SECTIONS_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionSectionModel) export class SubmissionSectionsModel extends SubmissionSectionModel { - static type = new ResourceType('submissionsections'); + static type = SUBMISSION_SECTIONS_TYPE; } diff --git a/src/app/core/config/models/config-submission-upload.model.ts b/src/app/core/config/models/config-submission-upload.model.ts new file mode 100644 index 0000000000..b08149659e --- /dev/null +++ b/src/app/core/config/models/config-submission-upload.model.ts @@ -0,0 +1,39 @@ +import { autoserialize, inheritSerialization, deserialize } from 'cerialize'; +import { typedObject, link } from '../../cache/builders/build-decorators'; +import { ConfigObject } from './config.model'; +import { AccessConditionOption } from './config-access-condition-option.model'; +import { SubmissionFormsModel } from './config-submission-forms.model'; +import { SUBMISSION_UPLOAD_TYPE, SUBMISSION_FORMS_TYPE } from './config-type'; +import { HALLink } from '../../shared/hal-link.model'; +import { RemoteData } from '../../data/remote-data'; +import { Observable } from 'rxjs'; + +@typedObject +@inheritSerialization(ConfigObject) +export class SubmissionUploadModel extends ConfigObject { + static type = SUBMISSION_UPLOAD_TYPE; + /** + * A list of available bitstream access conditions + */ + @autoserialize + accessConditionOptions: AccessConditionOption[]; + + /** + * An object representing the configuration describing the bitstream metadata form + */ + @link(SUBMISSION_FORMS_TYPE) + metadata?: Observable>; + + @autoserialize + required: boolean; + + @autoserialize + maxSize: number; + + @deserialize + _links: { + metadata: HALLink + self: HALLink + } + +} diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts index b7733ee25d..8fb7dc66b9 100644 --- a/src/app/core/config/models/config-submission-uploads.model.ts +++ b/src/app/core/config/models/config-submission-uploads.model.ts @@ -1,30 +1,10 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { inheritSerialization } from 'cerialize'; import { typedObject } from '../../cache/builders/build-decorators'; -import { ConfigObject } from './config.model'; -import { AccessConditionOption } from './config-access-condition-option.model'; -import { SubmissionFormsModel } from './config-submission-forms.model'; -import { ResourceType } from '../../shared/resource-type'; +import { SUBMISSION_UPLOADS_TYPE } from './config-type'; +import { SubmissionUploadModel } from './config-submission-upload.model'; @typedObject -@inheritSerialization(ConfigObject) -export class SubmissionUploadsModel extends ConfigObject { - static type = new ResourceType('submissionupload'); - /** - * A list of available bitstream access conditions - */ - @autoserialize - accessConditionOptions: AccessConditionOption[]; - - /** - * An object representing the configuration describing the bistream metadata form - */ - @autoserialize - metadata: SubmissionFormsModel; - - @autoserialize - required: boolean; - - @autoserialize - maxSize: number; - +@inheritSerialization(SubmissionUploadModel) +export class SubmissionUploadsModel extends SubmissionUploadModel { + static type = SUBMISSION_UPLOADS_TYPE; } diff --git a/src/app/core/config/models/config-type.ts b/src/app/core/config/models/config-type.ts index 91371f10f5..858ff19c91 100644 --- a/src/app/core/config/models/config-type.ts +++ b/src/app/core/config/models/config-type.ts @@ -1,10 +1,17 @@ -export enum ConfigType { - SubmissionDefinitions = 'submissiondefinitions', - SubmissionDefinition = 'submissiondefinition', - SubmissionForm = 'submissionform', - SubmissionForms = 'submissionforms', - SubmissionSections = 'submissionsections', - SubmissionSection = 'submissionsection', - SubmissionUploads = 'submissionuploads', - SubmissionUpload = 'submissionupload', -} +import { ResourceType } from '../../shared/resource-type'; + +export const SUBMISSION_DEFINITIONS_TYPE = new ResourceType('submissiondefinitions'); + +export const SUBMISSION_DEFINITION_TYPE = new ResourceType('submissiondefinition'); + +export const SUBMISSION_FORM_TYPE = new ResourceType('submissionform'); + +export const SUBMISSION_FORMS_TYPE = new ResourceType('submissionforms'); + +export const SUBMISSION_SECTIONS_TYPE = new ResourceType('submissionsections'); + +export const SUBMISSION_SECTION_TYPE = new ResourceType('submissionsection'); + +export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads'); + +export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload'); diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts deleted file mode 100644 index b7b0873c21..0000000000 --- a/src/app/core/config/submission-definitions-config.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@angular/core'; - -import { ConfigService } from './config.service'; -import { RequestService } from '../data/request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; - -@Injectable() -export class SubmissionDefinitionsConfigService extends ConfigService { - protected linkPath = 'submissiondefinitions'; - protected browseEndpoint = 'search/findByCollection'; - - constructor( - protected requestService: RequestService, - protected halService: HALEndpointService) { - super(); - } - -} diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index b688859ec9..25c39d915b 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -3,16 +3,38 @@ import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.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 { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { ConfigObject } from './models/config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { SUBMISSION_FORMS_TYPE } from './models/config-type'; +import { SubmissionFormsModel } from './models/config-submission-forms.model'; +import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @Injectable() +@dataService(SUBMISSION_FORMS_TYPE) export class SubmissionFormsConfigService extends ConfigService { - protected linkPath = 'submissionforms'; - protected browseEndpoint = ''; - constructor( protected requestService: RequestService, - protected halService: HALEndpointService) { - super(); + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer + ) { + super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionforms'); } + public findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return super.findByHref(href, reRequestOnStale, ...linksToFollow as Array>) as Observable>; + } } diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts deleted file mode 100644 index c8bbc0dd97..0000000000 --- a/src/app/core/config/submission-sections-config.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@angular/core'; - -import { ConfigService } from './config.service'; -import { RequestService } from '../data/request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; - -@Injectable() -export class SubmissionSectionsConfigService extends ConfigService { - protected linkPath = 'submissionsections'; - protected browseEndpoint = ''; - - constructor( - protected requestService: RequestService, - protected halService: HALEndpointService) { - super(); - } - -} diff --git a/src/app/core/config/submission-uploads-config.service.ts b/src/app/core/config/submission-uploads-config.service.ts index 2e092fa4f3..b8b5caf613 100644 --- a/src/app/core/config/submission-uploads-config.service.ts +++ b/src/app/core/config/submission-uploads-config.service.ts @@ -3,19 +3,40 @@ import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { SUBMISSION_UPLOADS_TYPE } from './models/config-type'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { ConfigObject } from './models/config.model'; +import { SubmissionUploadsModel } from './models/config-submission-uploads.model'; +import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. */ @Injectable() +@dataService(SUBMISSION_UPLOADS_TYPE) export class SubmissionUploadsConfigService extends ConfigService { - protected linkPath = 'submissionuploads'; - protected browseEndpoint = ''; - constructor( - protected objectCache: ObjectCacheService, protected requestService: RequestService, - protected halService: HALEndpointService) { - super(); + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer + ) { + super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionuploads'); + } + + findByHref(href: string, reRequestOnStale = true, ...linksToFollow): Observable> { + return super.findByHref(href, reRequestOnStale, ...linksToFollow as Array>) as Observable>; } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 2203377603..95e2726bcd 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -13,12 +13,12 @@ import { FormBuilderService } from '../shared/form/builder/form-builder.service' import { FormService } from '../shared/form/form.service'; import { HostWindowService } from '../shared/host-window.service'; import { MenuService } from '../shared/menu/menu.service'; -import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; +import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service'; import { MOCK_RESPONSE_MAP, mockResponseMap, ResponseMapMock -} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock'; +} from '../shared/mocks/dspace-rest/mocks/response-map.mock'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service'; @@ -28,27 +28,20 @@ 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 { AuthResponseParsingService } from './auth/auth-response-parsing.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'; import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { ObjectCacheService } from './cache/object-cache.service'; -import { ConfigResponseParsingService } from './config/config-response-parsing.service'; import { SubmissionDefinitionsModel } from './config/models/config-submission-definitions.model'; import { SubmissionFormsModel } from './config/models/config-submission-forms.model'; import { SubmissionSectionModel } from './config/models/config-submission-section.model'; import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model'; -import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; -import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { coreEffects } from './core.effects'; import { coreReducers, CoreState } from './core.reducers'; import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; -import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; -import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; -import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; import { CollectionDataService } from './data/collection-data.service'; import { CommunityDataService } from './data/community-data.service'; import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; @@ -62,13 +55,11 @@ 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 { FacetValueMapResponseParsingService } from './data/facet-value-map-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 { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { RelationshipTypeService } from './data/relationship-type.service'; @@ -76,9 +67,8 @@ import { RelationshipService } from './data/relationship.service'; import { ResourcePolicyService } from './resource-policy/resource-policy.service'; import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { SiteDataService } from './data/site-data.service'; -import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; +import { DspaceRestService } from './dspace-rest/dspace-rest.service'; import { EPersonDataService } from './eperson/eperson-data.service'; -import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; import { EPerson } from './eperson/models/eperson.model'; import { Group } from './eperson/models/group.model'; import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; @@ -139,7 +129,6 @@ import { Script } from '../process-page/scripts/script.model'; import { Process } from '../process-page/processes/process.model'; import { ProcessDataService } from './data/processes/process-data.service'; import { ScriptDataService } from './data/processes/script-data.service'; -import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { LocaleInterceptor } from './locale/locale.interceptor'; @@ -160,7 +149,6 @@ import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-licens import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; -import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service'; import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyService } from './submission/vocabularies/vocabulary.service'; import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service'; @@ -171,7 +159,9 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user- import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard'; import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; 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 { Root } from './data/root.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -179,7 +169,7 @@ import { UsageReport } from './statistics/models/usage-report.model'; */ export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => { if (environment.production) { - return new DSpaceRESTv2Service(http); + return new DspaceRestService(http); } else { return new EndpointMockingRestService(mocks, http); } @@ -199,20 +189,18 @@ const PROVIDERS = [ ApiService, AuthenticatedGuard, AuthRequestService, - AuthResponseParsingService, CommunityDataService, CollectionDataService, SiteDataService, DSOResponseParsingService, { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, - { provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, + { provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService, FormBuilderService, SectionFormOperationsService, FormService, - EpersonResponseParsingService, EPersonDataService, HALEndpointService, HostWindowService, @@ -226,24 +214,16 @@ const PROVIDERS = [ RemoteDataBuildService, EndpointMapResponseParsingService, FacetValueResponseParsingService, - FacetValueMapResponseParsingService, FacetConfigResponseParsingService, - MappedCollectionsReponseParsingService, DebugResponseParsingService, SearchResponseParsingService, MyDSpaceResponseParsingService, ServerResponseService, - BrowseResponseParsingService, - BrowseEntriesResponseParsingService, - BrowseItemsResponseParsingService, BrowseService, - ConfigResponseParsingService, SubmissionCcLicenseDataService, SubmissionCcLicenseUrlDataService, - SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionRestService, - SubmissionSectionsConfigService, SubmissionResponseParsingService, SubmissionJsonPatchOperationsService, JsonPatchOperationsBuilder, @@ -290,7 +270,6 @@ const PROVIDERS = [ WorkflowActionDataService, ProcessDataService, ScriptDataService, - ProcessFilesResponseParsingService, FeatureDataService, AuthorizationDataService, SiteAdministratorGuard, @@ -318,7 +297,6 @@ const PROVIDERS = [ FilteredDiscoveryPageResponseParsingService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, VocabularyService, - VocabularyEntriesResponseParsingService, VocabularyTreeviewService ]; @@ -327,6 +305,7 @@ const PROVIDERS = [ */ export const models = [ + Root, DSpaceObject, Bundle, Bitstream, @@ -373,6 +352,8 @@ export const models = VocabularyEntry, VocabularyEntryDetail, ConfigurationProperty, + ShortLivedToken, + Registration, UsageReport, ]; diff --git a/src/app/core/data/base-response-parsing.service.spec.ts b/src/app/core/data/base-response-parsing.service.spec.ts index 85f531e0ea..788986b3e4 100644 --- a/src/app/core/data/base-response-parsing.service.spec.ts +++ b/src/app/core/data/base-response-parsing.service.spec.ts @@ -18,7 +18,7 @@ class TestService extends BaseResponseParsingService { } public cache(obj, request: RestRequest, data: any) { - super.cache(obj, request, data); + super.cache(obj, request, data, undefined); } } @@ -62,7 +62,7 @@ describe('BaseResponseParsingService', () => { it('should call objectCache add', () => { service.cache(obj, request, {}); - expect(objectCache.add).toHaveBeenCalledWith(obj, request.responseMsToLive, request.uuid); + expect(objectCache.add).toHaveBeenCalledWith(obj, request.responseMsToLive, request.uuid, undefined); }); }); }); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index d69ebfbed5..2f90013f4f 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -1,14 +1,15 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { CacheableObject } from '../cache/object-cache.reducer'; import { Serializer } from '../serializer'; import { PageInfo } from '../shared/page-info.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GenericConstructor } from '../shared/generic-constructor'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList, buildPaginatedList } from './paginated-list.model'; import { getClassForType } from '../cache/builders/build-decorators'; import { RestRequest } from './request.models'; import { environment } from '../../../environments/environment'; + /* tslint:disable:max-classes-per-file */ /** @@ -28,10 +29,10 @@ export function isRestDataObject(halObj: any): boolean { */ export function isRestPaginatedList(halObj: any): boolean { return hasValue(halObj.page) && - hasValue(halObj.page.size) && - hasValue(halObj.page.totalElements) && - hasValue(halObj.page.totalPages) && - hasValue(halObj.page.number); + hasValue(halObj.page.size) && + hasValue(halObj.page.totalElements) && + hasValue(halObj.page.totalPages) && + hasValue(halObj.page.number); } export abstract class BaseResponseParsingService { @@ -40,7 +41,7 @@ export abstract class BaseResponseParsingService { protected shouldDirectlyAttachEmbeds = false; protected serializerConstructor: GenericConstructor> = DSpaceSerializer; - protected process(data: any, request: RestRequest): any { + protected process(data: any, request: RestRequest, alternativeURL?: string): any { if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; @@ -55,21 +56,21 @@ export abstract class BaseResponseParsingService { .keys(data._embedded) .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { - const parsedObj = this.process(data._embedded[property], request); + const parsedObj = this.process(data._embedded[property], request, data._links[property].href); if (hasValue(object) && this.shouldDirectlyAttachEmbeds && isNotEmpty(parsedObj)) { - if (isRestPaginatedList(data._embedded[property])) { - object[property] = parsedObj; - object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); - } else if (isRestDataObject(data._embedded[property])) { - object[property] = this.retrieveObjectOrUrl(parsedObj); - } else if (Array.isArray(parsedObj)) { - object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) - } + if (isRestPaginatedList(data._embedded[property])) { + object[property] = parsedObj; + object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); + } else if (isRestDataObject(data._embedded[property])) { + object[property] = this.retrieveObjectOrUrl(parsedObj); + } else if (Array.isArray(parsedObj)) { + object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) + } } }); } - this.cache(object, request, data); + this.cache(object, request, data, alternativeURL); return object; } const result = {}; @@ -95,7 +96,7 @@ export abstract class BaseResponseParsingService { list = this.flattenSingleKeyObject(list); } const page: ObjectDomain[] = this.processArray(list, request); - return new PaginatedList(pageInfo, page, ); + return buildPaginatedList(pageInfo, page,); } protected processArray(data: any, request: RestRequest): ObjectDomain[] { @@ -126,13 +127,13 @@ export abstract class BaseResponseParsingService { } } - protected cache(obj, request: RestRequest, data: any) { + protected cache(obj, request: RestRequest, data: any, alternativeURL: string) { if (this.toCache) { - this.addToObjectCache(obj, request, data); + this.addToObjectCache(obj, request, data, alternativeURL); } } - protected addToObjectCache(co: CacheableObject, request: RestRequest, data: any): void { + protected addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL: string): void { if (hasNoValue(co) || hasNoValue(co._links) || hasNoValue(co._links.self) || hasNoValue(co._links.self.href)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; @@ -146,7 +147,8 @@ export abstract class BaseResponseParsingService { console.warn(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`); return; } - this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid); + + this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, alternativeURL); } processPageInfo(payload: any): PageInfo { @@ -178,3 +180,4 @@ export abstract class BaseResponseParsingService { return statusCode >= 200 && statusCode < 300; } } +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index b328141d7b..efe4f269ee 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -10,6 +10,8 @@ import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support- import { PutRequest } from './request.models'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; describe('BitstreamDataService', () => { let service: BitstreamDataService; @@ -17,6 +19,7 @@ describe('BitstreamDataService', () => { let requestService: RequestService; let halService: HALEndpointService; let bitstreamFormatService: BitstreamFormatDataService; + let rdbService: RemoteDataBuildService; const bitstreamFormatHref = 'rest-api/bitstreamformats'; const bitstream = Object.assign(new Bitstream(), { @@ -42,8 +45,9 @@ describe('BitstreamDataService', () => { bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { getBrowseEndpoint: observableOf(bitstreamFormatHref) }); + rdbService = getMockRemoteDataBuildService(); - service = new BitstreamDataService(requestService, null, null, null, objectCache, halService, null, null, null, null, bitstreamFormatService); + service = new BitstreamDataService(requestService, rdbService, null, objectCache, halService, null, null, null, null, bitstreamFormatService); }); describe('when updating the bitstream\'s format', () => { diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index ca0338116f..124f11480c 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -16,22 +16,20 @@ import { Bundle } from '../shared/bundle.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { BundleDataService } from './bundle-data.service'; -import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList, buildPaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { RemoteDataError } from './remote-data-error'; import { FindListOptions, PutRequest } from './request.models'; import { RequestService } from './request.service'; import { BitstreamFormatDataService } from './bitstream-format-data.service'; import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { RestResponse } from '../cache/response.models'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { configureRequest } from '../shared/operators'; import { combineLatest as observableCombineLatest } from 'rxjs'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PageInfo } from '../shared/page-info.model'; +import { RequestEntryState } from './request.reducer'; /** * A service to retrieve {@link Bitstream}s from the REST API @@ -51,7 +49,6 @@ export class BitstreamDataService extends DataService { protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, @@ -66,11 +63,15 @@ export class BitstreamDataService extends DataService { /** * Retrieves the {@link Bitstream}s in a given bundle * - * @param bundle the bundle to retrieve bitstreams from - * @param options options for the find all request + * @param 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 */ - findAllByBundle(bundle: Bundle, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - return this.findAllByHref(bundle._links.bitstreams.href, options, ...linksToFollow); + findAllByBundle(bundle: Bundle, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(bundle._links.bitstreams.href, options, reRequestOnStale, ...linksToFollow); } /** @@ -86,11 +87,13 @@ export class BitstreamDataService extends DataService { map((bitstreamRD: RemoteData>) => { if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { return new RemoteData( - false, - false, - true, - undefined, - bitstreamRD.payload.page[0] + bitstreamRD.timeCompleted, + bitstreamRD.msToLive, + bitstreamRD.lastUpdated, + bitstreamRD.state, + bitstreamRD.errorMessage, + bitstreamRD.payload.page[0], + bitstreamRD.statusCode ); } else { return bitstreamRD as any; @@ -126,19 +129,23 @@ export class BitstreamDataService extends DataService { ); if (hasValue(matchingThumbnail)) { return new RemoteData( - false, - false, - true, - undefined, - matchingThumbnail + bitstreamRD.timeCompleted, + bitstreamRD.msToLive, + bitstreamRD.lastUpdated, + bitstreamRD.state, + bitstreamRD.errorMessage, + matchingThumbnail, + bitstreamRD.statusCode ); } else { return new RemoteData( - false, - false, - false, - new RemoteDataError(404, '404', 'No matching thumbnail found'), - undefined + bitstreamRD.timeCompleted, + bitstreamRD.msToLive, + bitstreamRD.lastUpdated, + RequestEntryState.Error, + 'No matching thumbnail found', + undefined, + 404 ); } } else { @@ -159,18 +166,21 @@ export class BitstreamDataService extends DataService { * The {@link Item} is technically redundant, but is available * in all current use cases, and having it simplifies this method * - * @param item the {@link Item} the {@link Bundle} is a part of - * @param bundleName the name of the {@link Bundle} we want to find {@link Bitstream}s for + * @param 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 linksToFollow the {@link FollowLinkConfig}s 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 */ - public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { return this.bundleService.findByItemAndName(item, bundleName).pipe( switchMap((bundleRD: RemoteData) => { if (bundleRD.hasSucceeded && hasValue(bundleRD.payload)) { - return this.findAllByBundle(bundleRD.payload, options, ...linksToFollow); - } else if (!bundleRD.hasSucceeded && bundleRD.error.statusCode === 404) { - return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + return this.findAllByBundle(bundleRD.payload, options, reRequestOnStale, ...linksToFollow); + } else if (!bundleRD.hasSucceeded && bundleRD.statusCode === 404) { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []), new Date().getTime()) } else { return [bundleRD as any]; } @@ -183,7 +193,7 @@ export class BitstreamDataService extends DataService { * @param bitstream * @param format */ - updateFormat(bitstream: Bitstream, format: BitstreamFormat): Observable { + updateFormat(bitstream: Bitstream, format: BitstreamFormat): Observable> { const requestId = this.requestService.generateRequestId(); const bitstreamHref$ = this.getBrowseEndpoint().pipe( map((href: string) => `${href}/${bitstream.id}`), @@ -206,9 +216,7 @@ export class BitstreamDataService extends DataService { this.requestService.removeByHrefSubstring(bitstream.self + '/format'); }); - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() - ); + return this.rdbService.buildFromRequestUUID(requestId); } } diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index 99bf4eea18..1df0ad23fd 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -18,6 +18,7 @@ import { } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { TestScheduler } from 'rxjs/testing'; import { CoreState } from '../core.reducers'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; describe('BitstreamFormatDataService', () => { let service: BitstreamFormatDataService; @@ -29,7 +30,6 @@ describe('BitstreamFormatDataService', () => { const responseCacheEntry = new RequestEntry(); responseCacheEntry.response = new RestResponse(true, 200, 'Success'); - responseCacheEntry.completed = true; const store = { dispatch(action: Action) { @@ -47,9 +47,16 @@ describe('BitstreamFormatDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const rdbService = {} as RemoteDataBuildService; + + let rd; + let rdbService: RemoteDataBuildService; function initTestService(halService) { + rd = createSuccessfulRemoteDataObject({}); + rdbService = jasmine.createSpyObj('rdbService', { + buildFromRequestUUID: observableOf(rd) + }); + return new BitstreamFormatDataService( requestService, rdbService, @@ -141,7 +148,7 @@ describe('BitstreamFormatDataService', () => { const updatedBistreamFormat = new BitstreamFormat(); updatedBistreamFormat.uuid = 'updated-uuid'; - const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const expected = cold('(b|)', {b: rd}); const result = service.updateBitstreamFormat(updatedBistreamFormat); expect(result).toBeObservable(expected); @@ -165,7 +172,7 @@ describe('BitstreamFormatDataService', () => { const newFormat = new BitstreamFormat(); newFormat.uuid = 'new-uuid'; - const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const expected = cold('(b|)', {b: rd}); const result = service.createBitstreamFormat(newFormat); expect(result).toBeObservable(expected); @@ -281,7 +288,7 @@ describe('BitstreamFormatDataService', () => { format.uuid = 'format-uuid'; format.id = 'format-id'; - const expected = cold('(b|)', { b: responseCacheEntry.response }); + const expected = cold('(b|)', { b: rd }); const result = service.delete(format.id); expect(result).toBeObservable(expected); diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 52ca07060a..6e31322946 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -3,31 +3,28 @@ import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged'; -import { find, map, tap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; -import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; 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, getResponseFromEntry } from '../shared/operators'; +import { configureRequest } from '../shared/operators'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RemoteData } from './remote-data'; -import { DeleteByIDRequest, PostRequest, PutRequest } from './request.models'; -import { RequestEntry } from './request.reducer'; +import { PostRequest, PutRequest } from './request.models'; import { RequestService } from './request.service'; const bitstreamFormatsStateSelector = createSelector( @@ -79,7 +76,7 @@ export class BitstreamFormatDataService extends DataService { * Update an existing bitstreamFormat * @param bitstreamFormat */ - updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable { + updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable> { const requestId = this.requestService.generateRequestId(); this.getUpdateEndpoint(bitstreamFormat.id).pipe( @@ -88,9 +85,7 @@ export class BitstreamFormatDataService extends DataService { new PutRequest(requestId, endpointURL, bitstreamFormat)), configureRequest(this.requestService)).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() - ); + return this.rdbService.buildFromRequestUUID(requestId); } @@ -98,7 +93,7 @@ export class BitstreamFormatDataService extends DataService { * Create a new BitstreamFormat * @param {BitstreamFormat} bitstreamFormat */ - public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable { + public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable> { const requestId = this.requestService.generateRequestId(); this.getCreateEndpoint().pipe( @@ -108,9 +103,7 @@ export class BitstreamFormatDataService extends DataService { configureRequest(this.requestService) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** @@ -152,31 +145,6 @@ export class BitstreamFormatDataService extends DataService { this.store.dispatch(new BitstreamFormatsRegistryDeselectAllAction()); } - /** - * Delete an existing DSpace Object on the server - * @param formatID The DSpace Object'id to be removed - * @return the RestResponse as an Observable - */ - delete(formatID: string): Observable { - const requestId = this.requestService.generateRequestId(); - - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, formatID))); - - hrefObs.pipe( - find((href: string) => hasValue(href)), - map((href: string) => { - const request = new DeleteByIDRequest(requestId, href, formatID); - this.requestService.configure(request); - }) - ).subscribe(); - - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) - ); - } - findByBitstream(bitstream: Bitstream): Observable> { return this.findByHref(bitstream._links.format.href); } diff --git a/src/app/core/data/browse-entries-response-parsing.service.spec.ts b/src/app/core/data/browse-entries-response-parsing.service.spec.ts deleted file mode 100644 index 3c7319d5cf..0000000000 --- a/src/app/core/data/browse-entries-response-parsing.service.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; -import { BrowseEntriesRequest } from './request.models'; - -describe('BrowseEntriesResponseParsingService', () => { - let service: BrowseEntriesResponseParsingService; - - beforeEach(() => { - service = new BrowseEntriesResponseParsingService(getMockObjectCacheService()); - }); - - describe('parse', () => { - const request = new BrowseEntriesRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/entries'); - - const validResponse = { - payload: { - _embedded: { - browseEntries: [ - { - authority: null, - value: 'Arulmozhiyal, Ramaswamy', - valueLang: null, - count: 1, - type: 'browseEntry', - _links: { - items: { - href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy' - } - } - }, - { - authority: null, - value: 'Bastida-Jumilla, Ma Consuelo', - valueLang: null, - count: 1, - type: 'browseEntry', - _links: { - items: { - href: 'https://rest.api/discover/browses/author/items?filterValue=Bastida-Jumilla, Ma Consuelo' - } - } - }, - { - authority: null, - value: 'Cao, Binggang', - valueLang: null, - count: 1, - type: 'browseEntry', - _links: { - items: { - href: 'https://rest.api/discover/browses/author/items?filterValue=Cao, Binggang' - } - } - }, - { - authority: null, - value: 'Castelli, Mauro', - valueLang: null, - count: 1, - type: 'browseEntry', - _links: { - items: { - href: 'https://rest.api/discover/browses/author/items?filterValue=Castelli, Mauro' - } - } - }, - { - authority: null, - value: 'Cat, Lily', - valueLang: null, - count: 1, - type: 'browseEntry', - _links: { - items: { - href: 'https://rest.api/discover/browses/author/items?filterValue=Cat, Lily' - } - } - } - ] - }, - _links: { - first: { - href: 'https://rest.api/discover/browses/author/entries?page=0&size=5' - }, - self: { - href: 'https://rest.api/discover/browses/author/entries' - }, - next: { - href: 'https://rest.api/discover/browses/author/entries?page=1&size=5' - }, - last: { - href: 'https://rest.api/discover/browses/author/entries?page=9&size=5' - } - }, - page: { - size: 5, - totalElements: 50, - totalPages: 10, - number: 0 - } - }, - statusCode: 200, - statusText: 'OK' - } as DSpaceRESTV2Response; - - const invalidResponseNotAList = { - statusCode: 200, - statusText: 'OK' - } as DSpaceRESTV2Response; - - const invalidResponseStatusCode = { - payload: {}, statusCode: 500, statusText: 'Internal Server Error' - } as DSpaceRESTV2Response; - - it('should return a GenericSuccessResponse if data contains a valid browse entries response', () => { - const response = service.parse(request, validResponse); - expect(response.constructor).toBe(GenericSuccessResponse); - }); - - it('should return an ErrorResponse if data contains an invalid browse entries response', () => { - const response = service.parse(request, invalidResponseNotAList); - expect(response.constructor).toBe(ErrorResponse); - }); - - it('should return an ErrorResponse if data contains a statuscode other than 200', () => { - const response = service.parse(request, invalidResponseStatusCode); - expect(response.constructor).toBe(ErrorResponse); - }); - - }); -}); diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts deleted file mode 100644 index 1009a07bca..0000000000 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { BrowseEntry } from '../shared/browse-entry.model'; -import { EntriesResponseParsingService } from './entries-response-parsing.service'; -import { GenericConstructor } from '../shared/generic-constructor'; - -@Injectable() -export class BrowseEntriesResponseParsingService extends EntriesResponseParsingService { - - protected toCache = false; - - constructor( - protected objectCache: ObjectCacheService, - ) { - super(objectCache); - } - - getSerializerModel(): GenericConstructor { - return BrowseEntry; - } - -} diff --git a/src/app/core/data/browse-items-response-parsing-service.spec.ts b/src/app/core/data/browse-items-response-parsing-service.spec.ts deleted file mode 100644 index a1b1a14bff..0000000000 --- a/src/app/core/data/browse-items-response-parsing-service.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; -import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models'; -import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; - -describe('BrowseItemsResponseParsingService', () => { - let service: BrowseItemsResponseParsingService; - - beforeEach(() => { - service = new BrowseItemsResponseParsingService(getMockObjectCacheService()); - }); - - describe('parse', () => { - const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items'); - - const validResponse = { - payload: { - _embedded: { - items: [ - { - id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', - uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', - name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India', - handle: '10986/17472', - metadata: { - 'dc.creator': [ - { - value: 'World Bank', - language: null - } - ] - }, - inArchive: true, - discoverable: true, - withdrawn: false, - lastModified: '2018-05-25T09:32:58.005+0000', - type: 'item', - _links: { - bitstreams: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams' - }, - owningCollection: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection' - }, - templateItemOf: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf' - }, - self: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7' - } - } - }, - { - id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b', - uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b', - name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India', - handle: '10986/17475', - metadata: { - 'dc.creator': [ - { - value: 'World Bank', - language: null - } - ] - }, - inArchive: true, - discoverable: true, - withdrawn: false, - lastModified: '2018-05-25T09:33:42.526+0000', - type: 'item', - _links: { - bitstreams: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams' - }, - owningCollection: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection' - }, - templateItemOf: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf' - }, - self: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b' - } - } - } - ] - }, - _links: { - first: { - href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2' - }, - self: { - href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items' - }, - next: { - href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2' - }, - last: { - href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2' - } - }, - page: { - size: 2, - totalElements: 16, - totalPages: 8, - number: 0 - } - }, - statusCode: 200, - statusText: 'OK' - } as DSpaceRESTV2Response; - - const invalidResponseNotAList = { - payload: { - id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', - uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', - name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India', - handle: '10986/17472', - metadata: { - 'dc.creator': [ - { - value: 'World Bank', - language: null - } - ] - }, - inArchive: true, - discoverable: true, - withdrawn: false, - lastModified: '2018-05-25T09:32:58.005+0000', - type: 'item', - _links: { - bitstreams: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams' - }, - owningCollection: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection' - }, - templateItemOf: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf' - }, - self: { - href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7' - } - } - }, - statusCode: 200, - statusText: 'OK' - } as DSpaceRESTV2Response; - - const invalidResponseStatusCode = { - payload: {}, statusCode: 500, statusText: 'Internal Server Error' - } as DSpaceRESTV2Response; - - it('should return a GenericSuccessResponse if data contains a valid browse items response', () => { - const response = service.parse(request, validResponse); - expect(response.constructor).toBe(GenericSuccessResponse); - }); - - it('should return an ErrorResponse if data contains an invalid browse entries response', () => { - const response = service.parse(request, invalidResponseNotAList); - expect(response.constructor).toBe(ErrorResponse); - }); - - it('should return an ErrorResponse if data contains a statuscode other than 200', () => { - const response = service.parse(request, invalidResponseStatusCode); - expect(response.constructor).toBe(ErrorResponse); - }); - - }); -}); diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts deleted file mode 100644 index 2b7ec647c9..0000000000 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; - -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -/** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[]) - */ -@Injectable() -export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected toCache = false; - - constructor( - protected objectCache: ObjectCacheService, - ) { super(); - } - - /** - * Parses data from the browse endpoint to a list of DSpaceObjects - * @param {RestRequest} request - * @param {DSpaceRESTV2Response} data - * @returns {RestResponse} - */ - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded) - && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceSerializer(DSpaceObject); - const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); - return new GenericSuccessResponse(items, data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else if (hasValue(data.payload) && hasValue(data.payload.page)) { - return new GenericSuccessResponse([], data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from browse endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); - } - } - -} diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts deleted file mode 100644 index fedfea1309..0000000000 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { BrowseDefinition } from '../shared/browse-definition.model'; -import { BrowseResponseParsingService } from './browse-response-parsing.service'; -import { BrowseEndpointRequest } from './request.models'; - -describe('BrowseResponseParsingService', () => { - let service: BrowseResponseParsingService; - - beforeEach(() => { - service = new BrowseResponseParsingService(); - }); - let validRequest; - let validResponse; - let invalidResponse1; - let invalidResponse2; - let invalidResponse3; - let definitions; - - describe('parse', () => { - beforeEach(() => { - validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses'); - - validResponse = { - payload: { - _embedded: { - browses: [{ - metadataBrowse: false, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { - name: 'dateissued', - metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - metadata: ['dc.date.issued'], - _links: { - self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } - } - }, { - metadataBrowse: true, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { - name: 'dateissued', - metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - metadata: ['dc.contributor.*', 'dc.creator'], - _links: { - self: { href: 'https://rest.api/discover/browses/author' }, - entries: { href: 'https://rest.api/discover/browses/author/entries' }, - items: { href: 'https://rest.api/discover/browses/author/items' } - } - }] - }, - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; - - invalidResponse1 = { - payload: { - _embedded: { - browse: { - metadataBrowse: false, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { - name: 'dateissued', - metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - metadata: ['dc.date.issued'], - _links: { - self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } - } - } - }, - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; - - invalidResponse2 = { - payload: { - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; - - invalidResponse3 = { - payload: { - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: 500, statusText: 'Internal Server Error' - } as DSpaceRESTV2Response; - - definitions = [ - Object.assign(new BrowseDefinition(), { - metadataBrowse: false, - sortOptions: [ - { - name: 'title', - metadata: 'dc.title' - }, - { - name: 'dateissued', - metadata: 'dc.date.issued' - }, - { - name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } - ], - defaultSortOrder: 'ASC', - metadataKeys: [ - 'dc.date.issued' - ], - _links: { - self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } - } - }), - Object.assign(new BrowseDefinition(), { - metadataBrowse: true, - sortOptions: [ - { - name: 'title', - metadata: 'dc.title' - }, - { - name: 'dateissued', - metadata: 'dc.date.issued' - }, - { - name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } - ], - defaultSortOrder: 'ASC', - metadataKeys: [ - 'dc.contributor.*', - 'dc.creator' - ], - _links: { - self: { href: 'https://rest.api/discover/browses/author' }, - entries: { href: 'https://rest.api/discover/browses/author/entries' }, - items: { href: 'https://rest.api/discover/browses/author/items' } - } - }) - ]; - }); - it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => { - const response = service.parse(validRequest, validResponse); - expect(response.constructor).toBe(GenericSuccessResponse); - }); - - it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => { - const response1 = service.parse(validRequest, invalidResponse1); - const response2 = service.parse(validRequest, invalidResponse2); - expect(response1.constructor).toBe(ErrorResponse); - expect(response2.constructor).toBe(ErrorResponse); - }); - - it('should return an ErrorResponse if data contains a statuscode other than 200', () => { - const response = service.parse(validRequest, invalidResponse3); - expect(response.constructor).toBe(ErrorResponse); - }); - - it('should return a GenericSuccessResponse with the BrowseDefinitions in data', () => { - const response = service.parse(validRequest, validResponse); - expect((response as GenericSuccessResponse).payload).toEqual(definitions); - }); - }); -}); diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts deleted file mode 100644 index d1b9c2f15c..0000000000 --- a/src/app/core/data/browse-response-parsing.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@angular/core'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { BrowseDefinition } from '../shared/browse-definition.model'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class BrowseResponseParsingService implements ResponseParsingService { - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded) - && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceSerializer(BrowseDefinition); - const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); - return new GenericSuccessResponse(browseDefinitions, data.statusCode, data.statusText); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from browse endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); - } - } -} diff --git a/src/app/core/data/bundle-data.service.spec.ts b/src/app/core/data/bundle-data.service.spec.ts index 6c63ca8978..8c14ca0a97 100644 --- a/src/app/core/data/bundle-data.service.spec.ts +++ b/src/app/core/data/bundle-data.service.spec.ts @@ -10,6 +10,9 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { BundleDataService } from './bundle-data.service'; import { HALLink } from '../shared/hal-link.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { Bundle } from '../shared/bundle.model'; class DummyChangeAnalyzer implements ChangeAnalyzer { diff(object1: Item, object2: Item): Operation[] { @@ -78,7 +81,54 @@ describe('BundleDataService', () => { }); it('should call findAllByHref with the item\'s bundles link', () => { - expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined); + expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined, true); }) }); + + describe('findByItemAndName', () => { + let bundles: Bundle[]; + + beforeEach(() => { + bundles = [ + Object.assign(new Bundle(), { + id: 'ORIGINAL_BUNDLE', + metadata: { + 'dc.title': [ + { + value: 'ORIGINAL' + } + ] + } + }), + Object.assign(new Bundle(), { + id: 'THUMBNAIL_BUNDLE', + metadata: { + 'dc.title': [ + { + value: 'THUMBNAIL' + } + ] + } + }), + Object.assign(new Bundle(), { + id: 'EXTRA_BUNDLE', + metadata: { + 'dc.title': [ + { + value: 'EXTRA' + } + ] + } + }), + ]; + spyOn(service, 'findAllByItem').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(bundles))); + }); + + it('should only return the requested bundle by name', (done) => { + service.findByItemAndName(undefined, 'THUMBNAIL').subscribe((result) => { + expect(result.payload.id).toEqual('THUMBNAIL_BUNDLE'); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index e651ed354f..88f060c3cd 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -16,13 +16,13 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { FindListOptions, GetRequest } from './request.models'; import { RequestService } from './request.service'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { Bitstream } from '../shared/bitstream.model'; -import { RemoteDataError } from './remote-data-error'; +import { RequestEntryState } from './request.reducer'; /** * A service to retrieve {@link Bundle}s from the REST API @@ -50,38 +50,52 @@ export class BundleDataService extends DataService { /** * Retrieve all {@link Bundle}s in the given {@link Item} * - * @param item the {@link Item} the {@link Bundle}s are a part of - * @param options the {@link FindListOptions} for the request - * @param 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 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 */ - findAllByItem(item: Item, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - return this.findAllByHref(item._links.bundles.href, options, ...linksToFollow); + findAllByItem(item: Item, options?: FindListOptions, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(item._links.bundles.href, options, 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 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 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 */ // TODO should be implemented rest side - findByItemAndName(item: Item, bundleName: string, ...linksToFollow: Array>): Observable> { - return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, ...linksToFollow).pipe( + findByItemAndName(item: Item, bundleName: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, reRequestOnStale, ...linksToFollow).pipe( map((rd: RemoteData>) => { if (hasValue(rd.payload) && hasValue(rd.payload.page)) { const matchingBundle = rd.payload.page.find((bundle: Bundle) => bundle.name === bundleName); if (hasValue(matchingBundle)) { return new RemoteData( - false, - false, - true, - undefined, - matchingBundle + rd.timeCompleted, + rd.msToLive, + rd.lastUpdated, + RequestEntryState.Success, + null, + matchingBundle, + 200 ); } else { - return new RemoteData(false, false, false, new RemoteDataError(404, 'Not found', `The bundle with name ${bundleName} was not found.` )) + return new RemoteData( + rd.timeCompleted, + rd.msToLive, + rd.lastUpdated, + RequestEntryState.Error, + `The bundle with name ${bundleName} was not found.`, + null, + 404 + ); } } else { return rd as any; diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts index 395af4a68c..df36edd281 100644 --- a/src/app/core/data/change-analyzer.ts +++ b/src/app/core/data/change-analyzer.ts @@ -1,11 +1,11 @@ import { Operation } from 'fast-json-patch/lib/core'; -import { CacheableObject } from '../cache/object-cache.reducer'; +import { TypedObject } from '../cache/object-cache.reducer'; /** * An interface to determine what differs between two * NormalizedObjects */ -export interface ChangeAnalyzer { +export interface ChangeAnalyzer { /** * Compare two objects and return their differences as a diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 4b0dee7df7..530f3c1580 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -8,17 +8,21 @@ import { getMockTranslateService } from '../../shared/mocks/translate.service.mo import { fakeAsync, tick } from '@angular/core/testing'; import { ContentSourceRequest, GetRequest, UpdateContentSourceRequest } from './request.models'; import { ContentSource } from '../shared/content-source.model'; -import { of as observableOf } from 'rxjs/internal/observable/of'; -import { RequestEntry } from './request.reducer'; -import { ErrorResponse } from '../cache/response.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { Collection } from '../shared/collection.model'; import { PageInfo } from '../shared/page-info.model'; -import { PaginatedList } from './paginated-list'; -import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; +import { buildPaginatedList } from './paginated-list.model'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from 'src/app/shared/remote-data.utils'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from './remote-data'; +import { hasNoValue } from '../../shared/empty.util'; const url = 'fake-url'; const collectionId = 'fake-collection-id'; @@ -68,17 +72,12 @@ describe('CollectionDataService', () => { const pageInfo = new PageInfo(); const array = [mockCollection1, mockCollection2, mockCollection3]; - const paginatedList = new PaginatedList(pageInfo, array); + const paginatedList = buildPaginatedList(pageInfo, array); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); describe('when the requests are successful', () => { beforeEach(() => { - createService(observableOf({ - request: { - href: 'https://rest.api/request' - }, - completed: true - })); + createService(); }); describe('when calling getContentSource', () => { @@ -164,13 +163,7 @@ describe('CollectionDataService', () => { describe('when the requests are unsuccessful', () => { beforeEach(() => { - createService(observableOf(Object.assign(new RequestEntry(), { - response: new ErrorResponse(Object.assign({ - statusCode: 422, - statusText: 'Unprocessable Entity', - message: 'Error message' - })) - }))); + createService(createFailedRemoteDataObject$('Error', 500)); }); describe('when calling updateContentSource', () => { @@ -198,14 +191,20 @@ describe('CollectionDataService', () => { /** * Create a CollectionDataService used for testing - * @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional) + * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) */ - function createService(requestEntry$?) { - requestService = getMockRequestService(requestEntry$); + function createService(reponse$?: Observable>) { + requestService = getMockRequestService(); + let buildResponse$ = reponse$; + if (hasNoValue(reponse$)) { + buildResponse$ = createSuccessfulRemoteDataObject$({}); + } rdbService = jasmine.createSpyObj('rdbService', { buildList: hot('a|', { a: paginatedListRD - }) + }), + buildFromRequestUUID: buildResponse$, + buildSingle: buildResponse$ }); objectCache = jasmine.createSpyObj('objectCache', { remove: jasmine.createSpy('remove') @@ -214,7 +213,7 @@ describe('CollectionDataService', () => { notificationsService = new NotificationsServiceStub(); translate = getMockTranslateService(); - service = new CollectionDataService(requestService, rdbService, null, null, objectCache, halService, notificationsService, null, null, translate); + service = new CollectionDataService(requestService, rdbService, null, null, objectCache, halService, notificationsService, null, null,null, translate); } }); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 41f70dd31c..cfc7962d4e 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -14,10 +14,9 @@ import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +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'; @@ -27,25 +26,23 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { configureRequest, - filterSuccessfulResponses, - getRequestFromRequestHref, - getResponseFromEntry + 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'; +import { PaginatedList } from './paginated-list.model'; import { ResponseParsingService } from './parsing.service'; import { RemoteData } from './remote-data'; import { ContentSourceRequest, FindListOptions, GetRequest, - RestRequest, UpdateContentSourceRequest } from './request.models'; import { RequestService } from './request.service'; +import { BitstreamDataService } from './bitstream-data.service'; @Injectable() @dataService(COLLECTION) @@ -63,6 +60,7 @@ export class CollectionDataService extends ComColDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, + protected bitstreamDataService: BitstreamDataService, protected comparator: DSOChangeAnalyzer, protected translate: TranslateService ) { @@ -74,16 +72,18 @@ export class CollectionDataService extends ComColDataService { * * @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 * @return Observable>> * collection list */ - getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + getAuthorizedCollection(query: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { const searchHref = 'findSubmitAuthorized'; options = Object.assign({}, options, { searchParams: [new RequestParam('query', query)] }); - return this.searchBy(searchHref, options, ...linksToFollow).pipe( + return this.searchBy(searchHref, options, reRequestOnStale, ...linksToFollow).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } @@ -141,15 +141,18 @@ export class CollectionDataService extends ComColDataService { * Get the collection's content harvester * @param collectionId */ - getContentSource(collectionId: string): Observable { - return this.getHarvesterEndpoint(collectionId).pipe( - map((href: string) => new ContentSourceRequest(this.requestService.generateRequestId(), href)), - configureRequest(this.requestService), - map((request: RestRequest) => request.href), - getRequestFromRequestHref(this.requestService), - filterSuccessfulResponses(), - map((response: ContentSourceSuccessResponse) => response.contentsource) + getContentSource(collectionId: string): Observable> { + const href$ = this.getHarvesterEndpoint(collectionId).pipe( + isNotEmptyOperator(), + take(1) ); + + href$.subscribe((href: string) => { + const request = new ContentSourceRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildSingle(href$); } /** @@ -177,11 +180,11 @@ export class CollectionDataService extends ComColDataService { ).subscribe(); // Return updated ContentSource - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful) { - if (hasValue((response as any).errorMessage)) { + return this.rdbService.buildFromRequestUUID(requestId).pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + if (response.hasFailed) { + if (hasValue(response.errorMessage)) { if (response.statusCode === 422) { return this.notificationsService.error(this.translate.instant(this.errorTitle), this.translate.instant(this.contentSourceError), new NotificationOptions(-1)); } else { @@ -193,9 +196,9 @@ export class CollectionDataService extends ComColDataService { } }), isNotEmptyOperator(), - map((response: ContentSourceSuccessResponse | INotification) => { - if (isNotEmpty((response as any).contentsource)) { - return (response as ContentSourceSuccessResponse).contentsource; + map((response: RemoteData | INotification) => { + if (isNotEmpty((response as any).payload)) { + return (response as RemoteData).payload; } return response as INotification; }) diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index f48022e6f1..80989ee00b 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -10,14 +10,17 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { Community } from '../shared/community.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; 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 { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; -import {createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject$, createNoContentRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { BitstreamDataService } from './bitstream-data.service'; const LINK_NAME = 'test'; @@ -32,6 +35,7 @@ class TestService extends ComColDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, + protected bitstreamDataService: BitstreamDataService, protected comparator: DSOChangeAnalyzer, protected linkPath: string ) { @@ -51,10 +55,9 @@ describe('ComColDataService', () => { let cds: CommunityDataService; let objectCache: ObjectCacheService; let halService: any = {}; + let bitstreamDataService: BitstreamDataService; + let rdbService: RemoteDataBuildService; - const rdbService = { - buildSingle : () => null - } as any; const store = {} as Store; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; @@ -80,6 +83,18 @@ describe('ComColDataService', () => { getEndpoint: (linkPath) => observableOf(communitiesEndpoint) }; + function initRdbService(): RemoteDataBuildService { + return jasmine.createSpyObj('rdbService', { + buildSingle : createFailedRemoteDataObject$('Error', 500) + }); + } + + function initBitstreamDataService(): BitstreamDataService { + return jasmine.createSpyObj('bitstreamDataService', { + deleteByHref: createSuccessfulRemoteDataObject$({}) + }); + } + function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { getEndpoint: hot('--a-', { a: communitiesEndpoint }), @@ -111,6 +126,7 @@ describe('ComColDataService', () => { halService, notificationsService, http, + bitstreamDataService, comparator, LINK_NAME ); @@ -120,6 +136,8 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = getMockRequestService(); objectCache = initMockObjectCacheService(); + bitstreamDataService = initBitstreamDataService(); + rdbService = initRdbService(); halService = mockHalService; service = initTestService(); }); @@ -143,36 +161,7 @@ describe('ComColDataService', () => { expect(requestService.configure).toHaveBeenCalledWith(expected); }); - describe('if the scope Community can be found', () => { - beforeEach(() => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(getRequestEntry$(true)); - objectCache = initMockObjectCacheService(); - service = initTestService(); - }); - - it('should fetch the scope Community from the cache', () => { - scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); - scheduler.flush(); - expect(objectCache.getObjectByUUID).toHaveBeenCalledWith(scopeID); - }); - - it('should return the endpoint to fetch resources within the given scope', () => { - const result = service.getBrowseEndpoint(options); - const expected = '--e-'; - - scheduler.expectObservable(result).toBe(expected, { e: scopedEndpoint }); - }); - }); - describe('if the scope Community can\'t be found', () => { - beforeEach(() => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(getRequestEntry$(false)); - objectCache = initMockObjectCacheService(); - service = initTestService(); - }); - it('should throw an error', () => { const result = service.getBrowseEndpoint(options); const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`)); @@ -186,16 +175,12 @@ describe('ComColDataService', () => { let data; beforeEach(() => { - scheduler = getTestScheduler(); - halService = { - getEndpoint: (linkPath) => 'https://rest.api/core/' + linkPath - }; - service = initTestService(); + spyOn(halService, 'getEndpoint').and.returnValue(observableOf('https://rest.api/core/communities/search/top')); + }); - }) describe('cache refreshed top level community', () => { beforeEach(() => { - spyOn(rdbService, 'buildSingle').and.returnValue(createNoContentRemoteDataObject$()); + (rdbService.buildSingle as jasmine.Spy).and.returnValue(createNoContentRemoteDataObject$()); data = { dso: Object.assign(new Community(), { metadata: [{ @@ -242,7 +227,7 @@ describe('ComColDataService', () => { }], _links: {} }); - spyOn(rdbService, 'buildSingle').and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity)); + (rdbService.buildSingle as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity)); data = { dso: Object.assign(new Community(), { metadata: [{ diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index a06a6bac9a..2113a237c5 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,44 +1,29 @@ -import { - distinctUntilChanged, - filter, first,map, mergeMap, share, switchMap, - take, - tap -} from 'rxjs/operators'; -import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs'; +import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Community } from '../shared/community.model'; import { HALLink } from '../shared/hal-link.model'; -import { HALResource } from '../shared/hal-resource.model'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; -import { DeleteRequest, FindListOptions, FindByIDRequest, RestRequest } from './request.models'; -import { PaginatedList } from './paginated-list'; +import { FindListOptions, FindByIDRequest } from './request.models'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { - configureRequest, - getRemoteDataPayload, - getResponseFromEntry, - getSucceededOrNoContentResponse, - getSucceededRemoteData -} from '../shared/operators'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { RestResponse } from '../cache/response.models'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { Bitstream } from '../shared/bitstream.model'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import {Collection} from '../shared/collection.model'; +import { Collection } from '../shared/collection.model'; +import { BitstreamDataService } from './bitstream-data.service'; +import { NoContent } from '../shared/NoContent.model'; +import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { URLCombiner } from '../url-combiner/url-combiner'; -export abstract class ComColDataService extends DataService { +export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; - - /** - * Linkpath of endpoint to delete the logo - */ - protected logoDeleteLinkpath = 'bitstreams'; + protected abstract bitstreamDataService: BitstreamDataService; /** * Get the scoped endpoint URL by fetching the object with @@ -63,23 +48,20 @@ export abstract class ComColDataService extends DataS this.requestService.configure(request); })); - const responses = scopeCommunityHrefObs.pipe( - mergeMap((href: string) => this.requestService.getByHref(href)), - getResponseFromEntry() - ); - const errorResponses = responses.pipe( - filter((response) => !response.isSuccessful), - mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))) - ); - const successResponses = responses.pipe( - filter((response) => response.isSuccessful), - mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)), - map((hr: HALResource) => hr._links[linkPath]), + return scopeCommunityHrefObs.pipe( + switchMap((href: string) => this.rdbService.buildSingle(href)), + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + if (response.hasFailed) { + throw new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`); + } else { + return response.payload._links[linkPath] + } + }), filter((halLink: HALLink) => isNotEmpty(halLink)), - map((halLink: HALLink) => halLink.href) + map((halLink: HALLink) => halLink.href), + distinctUntilChanged() ); - - return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share()); } } @@ -98,27 +80,34 @@ export abstract class ComColDataService extends DataS */ public getLogoEndpoint(id: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((href: string) => this.halService.getEndpoint('logo', `${href}/${id}`)) - ) + // We can't use HalLinkService to discover the logo link itself, as objects without a logo + // don't have the link, and this method is also used in the createLogo method. + map((href: string) => new URLCombiner(href, id, 'logo').toString()) + ); } /** * Delete the logo from the community or collection * @param dso The object to delete the logo from */ - public deleteLogo(dso: DSpaceObject): Observable { - const logo$ = (dso as any).logo; + public deleteLogo(dso: T): Observable> { + const logo$ = dso.logo; if (hasValue(logo$)) { - return observableCombineLatest( - logo$.pipe(getSucceededRemoteData(), getRemoteDataPayload(), take(1)), - this.halService.getEndpoint(this.logoDeleteLinkpath) - ).pipe( - map(([logo, href]: [Bitstream, string]) => `${href}/${logo.id}`), - map((href: string) => new DeleteRequest(this.requestService.generateRequestId(), href)), - configureRequest(this.requestService), - switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), - getResponseFromEntry() + // We need to fetch the logo before deleting it, because rest doesn't allow us to send a + // DELETE request to a `/logo` link. So we need to use the bitstream self link. + return logo$.pipe( + getFirstCompletedRemoteData(), + switchMap((logoRd: RemoteData) => { + if (logoRd.hasFailed) { + console.error(`Couldn't retrieve the logo '${dso._links.logo.href}' in order to delete it.`); + return [logoRd]; + } else { + return this.bitstreamDataService.deleteByHref(logoRd.payload._links.self.href); + } + }) ); + } else { + return createFailedRemoteDataObject$(`The given object doesn't have a logo`, 400); } } @@ -128,11 +117,15 @@ export abstract class ComColDataService extends DataS return; } this.findByHref(parentCommunityUrl).pipe( - getSucceededOrNoContentResponse(), - take(1), + getFirstCompletedRemoteData(), ).subscribe((rd: RemoteData) => { - const href = rd.hasSucceeded && !isEmpty(rd.payload.id) ? rd.payload.id : this.halService.getEndpoint('communities/search/top'); - this.requestService.removeByHrefSubstring(href) + if (rd.hasSucceeded && isNotEmpty(rd.payload) && isNotEmpty(rd.payload.id)) { + this.requestService.removeByHrefSubstring(rd.payload.id) + } else { + this.halService.getEndpoint('communities/search/top') + .pipe(take(1)) + .subscribe((href) => this.requestService.removeByHrefSubstring(href)); + } }); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 474bdef44a..daf8639cb4 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -15,10 +15,11 @@ import { COMMUNITY } from '../shared/community.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { FindListOptions, FindListRequest } from './request.models'; import { RequestService } from './request.service'; +import { BitstreamDataService } from './bitstream-data.service'; @Injectable() @dataService(COMMUNITY) @@ -34,6 +35,7 @@ export class CommunityDataService extends ComColDataService { protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, + protected bitstreamDataService: BitstreamDataService, protected http: HttpClient, protected comparator: DSOChangeAnalyzer ) { diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts index ec8221ebec..91d5af6ecc 100644 --- a/src/app/core/data/configuration-data.service.ts +++ b/src/app/core/data/configuration-data.service.ts @@ -60,3 +60,4 @@ export class ConfigurationDataService { return this.dataService.findById(name); } } +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index 95e25db613..4db9171754 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -1,20 +1,19 @@ import { Injectable } from '@angular/core'; -import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { ContentSource } from '../shared/content-source.model'; import { MetadataConfig } from '../shared/metadata-config.model'; -import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; @Injectable() /** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a ContentSource object - * wrapped in a ContentSourceSuccessResponse + * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a ContentSource object */ -export class ContentSourceResponseParsingService implements ResponseParsingService { +export class ContentSourceResponseParsingService extends DspaceRestResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + parse(request: RestRequest, data: RawRestResponse): ParsedResponse { const payload = data.payload; const deserialized = new DSpaceSerializer(ContentSource).deserialize(payload); @@ -25,7 +24,9 @@ export class ContentSourceResponseParsingService implements ResponseParsingServi } deserialized.metadataConfigs = metadataConfigs; - return new ContentSourceSuccessResponse(deserialized, data.statusCode, data.statusText); + this.addToObjectCache(deserialized, request, data) + + return new ParsedResponse(data.statusCode, deserialized._links.self); } } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 31013c5132..9e70f6c8b4 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -19,6 +19,7 @@ import { RequestService } from './request.service'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RequestParam } from '../cache/models/request-param.model'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; const endpoint = 'https://rest.api/core'; @@ -66,7 +67,7 @@ describe('DataService', () => { function initTestService(): TestService { requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; - rdbService = {} as RemoteDataBuildService; + rdbService = getMockRemoteDataBuildService(); notificationsService = {} as NotificationsService; http = {} as HttpClient; comparator = new DummyChangeAnalyzer() as any; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e3f367c8bf..08c0285a64 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -2,8 +2,17 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { + distinctUntilChanged, + filter, + find, + first, + map, + mergeMap, + take, + takeWhile, switchMap, tap, +} from 'rxjs/operators'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -12,22 +21,55 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestParam } from '../cache/models/request-param.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ErrorResponse, RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } 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'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { CreateRequest, DeleteByIDRequest, FindByIDRequest, FindListOptions, FindListRequest, GetRequest, PatchRequest, PutRequest } from './request.models'; -import { RequestEntry } from './request.reducer'; +import { + CreateRequest, + FindByIDRequest, + FindListOptions, + FindListRequest, + GetRequest, + PatchRequest, + PutRequest, DeleteRequest +} from './request.models'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; import { UpdateDataService } from './update-data.service'; import { GenericConstructor } from '../shared/generic-constructor'; +import { NoContent } from '../shared/NoContent.model'; + +/** + * An operator that will call the given function if the incoming RemoteData is stale and + * shouldReRequest is true + * + * @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale + * @param requestFn The function to call if the RemoteData is stale and shouldReRequest is + * true + */ +export const reRequestStaleRemoteData = (shouldReRequest: boolean, requestFn: () => Observable>) => + (source: Observable>): Observable> => { + if (shouldReRequest === true) { + return source.pipe( + tap((remoteData: RemoteData) => { + if (hasValue(remoteData) && remoteData.isStale) { + requestFn(); + } + }) + ) + } else { + return source; + } + }; export abstract class DataService implements UpdateDataService { protected abstract requestService: RequestService; @@ -206,34 +248,45 @@ export abstract class DataService implements UpdateDa * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded * info should be added to the objects * - * @param options Find list options object - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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 * @return {Observable>>} * Return an observable that emits object list */ - findAll(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.findList(this.getFindAllHref(options), options, ...linksToFollow); + findAll(options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.findList(this.getFindAllHref(options), options, reRequestOnStale, ...linksToFollow); } /** * Returns an observable of {@link RemoteData} of an object, based on href observable, * with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href$ Observable of href of object we want to retrieve - * @param options Find list options object - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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, ...linksToFollow: Array>) { + protected findList(href$, options: FindListOptions, reRequestOnStale = true, ...linksToFollow: Array>) { + const requestId = this.requestService.generateRequestId(); + href$.pipe( first((href: string) => hasValue(href))) .subscribe((href: string) => { - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); + const request = new FindListRequest(requestId, href, options); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } this.requestService.configure(request); }); - return this.rdbService.buildList(href$, ...linksToFollow) as Observable>>; + return this.rdbService.buildList(href$, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.findList(href$, options, reRequestOnStale, ...linksToFollow)) + ) as Observable>>; } /** @@ -257,58 +310,81 @@ export abstract class DataService implements UpdateDa } /** - * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the object - * @param id ID of object we want to retrieve - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * 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 */ - findById(id: string, ...linksToFollow: Array>): Observable> { - const hrefObs = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow); + findById(id: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + const requestId = this.requestService.generateRequestId(); - hrefObs.pipe( - find((href: string) => hasValue(href))) - .subscribe((href: string) => { - const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }); + const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow).pipe( + isNotEmptyOperator(), + take(1) + ); - return this.rdbService.buildSingle(hrefObs, ...linksToFollow); + href$.subscribe((href: string) => { + const request = new FindByIDRequest(requestId, href, id); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + }); + + return this.rdbService.buildSingle(href$, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.findById(id, reRequestOnStale, ...linksToFollow)) + ); } /** - * 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 linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * 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 */ - findByHref(href: string, ...linksToFollow: Array>): Observable> { + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { const requestHref = this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow); - const request = new GetRequest(this.requestService.generateRequestId(), requestHref); + const requestId = this.requestService.generateRequestId(); + const request = new GetRequest(requestId, requestHref); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } this.requestService.configure(request); - return this.rdbService.buildSingle(href, ...linksToFollow); + return this.rdbService.buildSingle(href, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.findByHref(href, 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 linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * 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 */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { const requestHref = this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow); - const request = new GetRequest(this.requestService.generateRequestId(), requestHref); + const requestId = this.requestService.generateRequestId(); + const request = new GetRequest(requestId, requestHref); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } this.requestService.configure(request); - return this.rdbService.buildList(requestHref, ...linksToFollow); + return this.rdbService.buildList(requestHref, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow)) + ); } /** @@ -325,13 +401,15 @@ export abstract class DataService implements UpdateDa /** * Make a new FindListRequest with given search method * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param linksToFollow The array of [[FollowLinkConfig]] + * @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]] * @return {Observable>} * Return an observable that emits response from the server */ - searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + searchBy(searchMethod: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { const requestId = this.requestService.generateRequestId(); const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); @@ -345,47 +423,40 @@ export abstract class DataService implements UpdateDa this.requestService.configure(request); }); - return this.requestService.getByUUID(requestId).pipe( - find((requestEntry) => hasValue(requestEntry) && requestEntry.completed), - switchMap((requestEntry) => - this.rdbService.buildList(requestEntry.request.href, ...linksToFollow) - ), + return this.rdbService.buildList(hrefObs, ...linksToFollow).pipe( + reRequestStaleRemoteData(reRequestOnStale, () => + this.searchBy(searchMethod, options, reRequestOnStale, ...linksToFollow)) ); } /** * Send a patch request for a specified object - * @param {T} dso The object to send a patch request for + * @param {T} object The object to send a patch request for * @param {Operation[]} operations The patch operations to be performed */ - patch(dso: T, operations: Operation[]): Observable { + patch(object: T, operations: Operation[]): Observable> { const requestId = this.requestService.generateRequestId(); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, dso.uuid))); + map((endpoint: string) => this.getIDHref(endpoint, object.uuid))); hrefObs.pipe( find((href: string) => hasValue(href)), - map((href: string) => { - const request = new PatchRequest(requestId, href, operations); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }) - ).subscribe(); + ).subscribe((href: string) => { + const request = new PatchRequest(requestId, href, operations); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + }); - return this.requestService.getByUUID(requestId).pipe( - hasValueOperator(), - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) - ); + return this.rdbService.buildFromRequestUUID(requestId); } createPatchFromCache(object: T): Observable { - const oldVersion$ = this.findByHref(object._links.self.href); + const oldVersion$ = this.findByHref(object._links.self.href, false); return oldVersion$.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((oldVersion: T) => this.comparator.diff(oldVersion, object))); } @@ -406,10 +477,7 @@ export abstract class DataService implements UpdateDa this.requestService.configure(request); - return this.requestService.getByUUID(requestId).pipe( - find((re: RequestEntry) => hasValue(re) && re.completed), - switchMap(() => this.findByHref(object._links.self.href)) - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** @@ -424,7 +492,7 @@ export abstract class DataService implements UpdateDa if (isNotEmpty(operations)) { this.objectCache.addPatch(object._links.self.href, operations); } - return this.findByHref(object._links.self.href); + return this.findByHref(object._links.self.href, true); } ) ); @@ -434,12 +502,12 @@ export abstract class DataService implements UpdateDa * Create a new DSpaceObject on the server, and store the response * in the object cache * - * @param {DSpaceObject} dso + * @param {CacheableObject} object * The object to create * @param {RequestParam[]} params * Array with additional params to combine with query string */ - create(dso: T, ...params: RequestParam[]): Observable> { + create(object: T, ...params: RequestParam[]): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.getEndpoint().pipe( isNotEmptyOperator(), @@ -447,129 +515,73 @@ export abstract class DataService implements UpdateDa map((endpoint: string) => this.buildHrefWithParams(endpoint, params)) ); - const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); + const serializedObject = new DSpaceSerializer(getClassForType(object.type)).serialize(object); - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - return request - }) - ); + endpoint$.pipe( + take(1) + ).subscribe((endpoint: string) => { + const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedObject)); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + }); - // Execute the post request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); + const result$ = this.rdbService.buildFromRequestUUID(requestId); - // Resolve self link for new object - const selfLink$ = this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful && response instanceof ErrorResponse) { - this.notificationsService.error('Server Error:', response.errorMessage, new NotificationOptions(-1)); - } else { - return response; - } - }), - map((response: any) => { - if (isNotEmpty(response.resourceSelfLinks)) { - return response.resourceSelfLinks[0]; - } - }), - distinctUntilChanged() - ) as Observable; + // TODO a dataservice is not the best place to show a notification, + // this should move up to the components that use this method + result$.pipe( + takeWhile((rd: RemoteData) => rd.isLoading, true) + ).subscribe((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1)); + } + }); - return selfLink$.pipe( - switchMap((selfLink: string) => this.findByHref(selfLink)), - ) + return result$; } /** - * Create a new DSpaceObject on the server, and store the response - * in the object cache, returns observable of the response to determine success - * - * @param {DSpaceObject} dso - * The object to create + * Delete an existing DSpace Object on the server + * @param objectId The id of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc */ - tryToCreate(dso: T): Observable { - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( - isNotEmptyOperator(), - distinctUntilChanged(), - ); - - const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - return request - }) - ); - - // Execute the post request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - return this.fetchResponse(requestId); - } - - /** - * Gets the restResponse from the requestService - * @param requestId - */ - protected fetchResponse(requestId: string): Observable { - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - return response; - }) + delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.getIDHrefObs(objectId).pipe( + switchMap((href: string) => this.deleteByHref(href, copyVirtualMetadata)) ); } /** * Delete an existing DSpace Object on the server - * @param dsoID The DSpace Object' id to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * @param href The self link of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual * metadata should be saved as real metadata - * @return the RestResponse as an Observable + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc */ - delete(dsoID: string, copyVirtualMetadata?: string[]): Observable { + deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getIDHrefObs(dsoID); + if (copyVirtualMetadata) { + copyVirtualMetadata.forEach((id) => + href += (href.includes('?') ? '&' : '?') + + 'copyVirtualMetadata=' + + id + ); + } - hrefObs.pipe( - find((href: string) => hasValue(href)), - map((href: string) => { - if (copyVirtualMetadata) { - copyVirtualMetadata.forEach((id) => - href += (href.includes('?') ? '&' : '?') - + 'copyVirtualMetadata=' - + id - ); - } - const request = new DeleteByIDRequest(requestId, href, dsoID); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.configure(request); - }) - ).subscribe(); + const request = new DeleteRequest(requestId, href); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts index 174abec897..fbc07cbb39 100644 --- a/src/app/core/data/debug-response-parsing.service.ts +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@angular/core'; import { RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; @Injectable() export class DebugResponseParsingService implements ResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + parse(request: RestRequest, data: RawRestResponse): RestResponse { console.log('request', request, 'data', data); return undefined; } diff --git a/src/app/core/data/default-change-analyzer.service.ts b/src/app/core/data/default-change-analyzer.service.ts index 20218925fb..0c3510172b 100644 --- a/src/app/core/data/default-change-analyzer.service.ts +++ b/src/app/core/data/default-change-analyzer.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { compare } from 'fast-json-patch'; import { Operation } from 'fast-json-patch/lib/core'; import { getClassForType } from '../cache/builders/build-decorators'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { TypedObject } from '../cache/object-cache.reducer'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { ChangeAnalyzer } from './change-analyzer'; /** @@ -11,7 +11,7 @@ import { ChangeAnalyzer } from './change-analyzer'; * CacheableObjects */ @Injectable() -export class DefaultChangeAnalyzer implements ChangeAnalyzer { +export class DefaultChangeAnalyzer implements ChangeAnalyzer { /** * Compare the metadata of two CacheableObject and return the differences as * a JsonPatch Operation Array diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts index ca62347883..a9aaf473c3 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -11,6 +11,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DsoRedirectDataService } from './dso-redirect-data.service'; import { FindByIDRequest, IdentifierType } from './request.models'; import { RequestService } from './request.service'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; describe('DsoRedirectDataService', () => { let scheduler: TestScheduler; @@ -47,16 +48,10 @@ describe('DsoRedirectDataService', () => { navigate: jasmine.createSpy('navigate') }; - remoteData = { - isSuccessful: true, - error: undefined, - hasSucceeded: true, - isLoading: false, - payload: { - type: 'item', - uuid: '123456789' - } - }; + remoteData = createSuccessfulRemoteDataObject({ + type: 'item', + uuid: '123456789' + }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts index 8ae7873c55..0ff1d36cc9 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { filter, take, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -16,6 +16,7 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RemoteData } from './remote-data'; import { FindByIDRequest, IdentifierType } from './request.models'; import { RequestService } from './request.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; @Injectable() export class DsoRedirectDataService extends DataService { @@ -55,8 +56,7 @@ export class DsoRedirectDataService extends DataService { findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable> { this.setLinkPath(identifierType); return this.findById(id).pipe( - filter((response) => hasValue(response.error) || hasValue(response.payload)), - take(1), + getFirstCompletedRemoteData(), tap((response) => { if (response.hasSucceeded) { const uuid = response.payload.uuid; diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index a4d4941bc8..0dfdffaf53 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; import { RestRequest } from './request.models'; @@ -20,7 +20,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem super(); } - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + parse(request: RestRequest, data: RawRestResponse): RestResponse { let processRequestDTO; // Prevent empty pages returning an error, initialize empty array instead. if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) { diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index ca67ca2e85..be71e71fc2 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -15,6 +15,8 @@ import { DataService } from './data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; +import { FindListOptions } from './request.models'; +import { PaginatedList } from './paginated-list.model'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -55,11 +57,42 @@ export class DSpaceObjectDataService { this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } - findById(uuid: string): Observable> { - return this.dataService.findById(uuid); + /** + * 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 + */ + findById(id: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findById(id, 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 + */ + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); } - findByHref(href: string): Observable> { - return this.dataService.findByHref(href); + /** + * 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 + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); } + } +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts new file mode 100644 index 0000000000..1d06957368 --- /dev/null +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -0,0 +1,265 @@ +import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { Serializer } from '../serializer'; +import { PageInfo } from '../shared/page-info.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { PaginatedList, buildPaginatedList } from './paginated-list.model'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { RestRequest } from './request.models'; +import { environment } from '../../../environments/environment'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { ParsedResponse } from '../cache/response.models'; +import { RestRequestMethod } from './rest-request-method'; +import { getUrlWithoutEmbedParams } from '../index/index.selectors'; + +/* tslint:disable:max-classes-per-file */ + +/** + * Return true if obj has a value for `_links.self` + * + * @param {any} obj The object to test + */ +export function isCacheableObject(obj: any): boolean { + return hasValue(obj) && hasValue(obj._links) && hasValue(obj._links.self) && hasValue(obj._links.self.href); +} + +/** + * Return true if halObj has a value for `page` with properties + * `size`, `totalElements`, `totalPages`, `number` + * + * @param {any} halObj The object to test + */ +export function isRestPaginatedList(halObj: any): boolean { + return hasValue(halObj.page) && + hasValue(halObj.page.size) && + hasValue(halObj.page.totalElements) && + hasValue(halObj.page.totalPages) && + hasValue(halObj.page.number); +} + +/** + * Split a url into parts + * + * @param url the url to split + */ +const splitUrlInParts = (url: string): string[] => { + return url.split('?') + .map((part) => part.split('&')) + .reduce((combined, current) => [...combined, ...current]) +} + +@Injectable({ providedIn: 'root' }) +export class DspaceRestResponseParsingService implements ResponseParsingService { + protected serializerConstructor: GenericConstructor> = DSpaceSerializer; + + constructor( + protected objectCache: ObjectCacheService, + ) { + } + + parse(request: RestRequest, response: RawRestResponse): ParsedResponse { + response = this.ensureSelfLink(request, response); + + let alternativeURL: string; + if (request.method === RestRequestMethod.GET) { + // only store an alternative URL when parsing a GET request, as there are cases when e.g. a + // POST or a PUT would have a different response + alternativeURL = getUrlWithoutEmbedParams(request.href); + } + + const processRequestDTO = this.process(response.payload, request, alternativeURL); + + if (hasValue(processRequestDTO)) { + if (isCacheableObject(processRequestDTO)) { + return new ParsedResponse(response.statusCode, processRequestDTO._links.self); + } else { + return new ParsedResponse(response.statusCode, undefined, processRequestDTO); + } + } else { + return new ParsedResponse(response.statusCode); + } + } + + public process(data: any, request: RestRequest, alternativeURL?: string): any { + if (isNotEmpty(data)) { + if (hasNoValue(data) || (typeof data !== 'object')) { + return data; + } else if (isRestPaginatedList(data)) { + return this.processPaginatedList(data, request, alternativeURL); + } else if (Array.isArray(data)) { + return this.processArray(data, request); + } else if (isCacheableObject(data)) { + const object = this.deserialize(data); + if (isNotEmpty(data._embedded)) { + Object + .keys(data._embedded) + .filter((property) => data._embedded.hasOwnProperty(property)) + .forEach((property) => { + this.process(data._embedded[property], request, data._links[property].href); + }); + } + + this.addToObjectCache(object, request, data, alternativeURL); + return object; + } + const result = {}; + Object.keys(data) + .filter((property) => data.hasOwnProperty(property)) + .filter((property) => hasValue(data[property])) + .forEach((property) => { + result[property] = this.process(data[property], request); + }); + return result; + + } + } + + /** + * Some rest endpoints don't return a self link in their response. This method will fix that for + * the root resource in the response by filling in the requested href, without any embed params. + * It will print a warning in the console, as this could indicate an issue on the REST side. + * + * @param request the {@RestRequest} that was sent to the server + * @param response the {@link RawRestResponse} returned by the server + * @protected + */ + protected ensureSelfLink(request: RestRequest, response: RawRestResponse): RawRestResponse { + const urlWithoutEmbedParams = getUrlWithoutEmbedParams(request.href); + if (request.method === RestRequestMethod.GET && hasValue(response) && hasValue(response.payload) && hasValue(response.payload._links)) { + if (hasNoValue(response.payload._links.self) || hasNoValue(response.payload._links.self.href)) { + console.warn(`The response for '${request.href}' doesn't have a self link. This could mean there's an issue with the REST endpoint`); + response.payload._links = Object.assign({}, response.payload._links, { + self: { + href: urlWithoutEmbedParams + } + }) + + } else { + const expected = splitUrlInParts(urlWithoutEmbedParams); + const actual = splitUrlInParts(response.payload._links.self.href); + if (expected[0] === actual[0] && (expected.some((e) => !actual.includes(e)) || actual.some((e) => !expected.includes(e)))) { + console.warn(`The response for '${urlWithoutEmbedParams}' has the self link '${response.payload._links.self.href}'. These don't match. This could mean there's an issue with the REST endpoint`); + response.payload._links = Object.assign({}, response.payload._links, { + self: { + href: urlWithoutEmbedParams + } + }) + } + } + } + return response; + } + + protected processPaginatedList(data: any, request: RestRequest, alternativeURL?: string): PaginatedList { + const pageInfo: PageInfo = this.processPageInfo(data); + let list = data._embedded; + + // Workaround for inconsistency in rest response. Issue: https://github.com/DSpace/dspace-angular/issues/238 + if (hasNoValue(list)) { + list = []; + } else if (!Array.isArray(list)) { + list = this.flattenSingleKeyObject(list); + } + + const page: ObjectDomain[] = this.processArray(list, request); + const paginatedList = buildPaginatedList(pageInfo, page, true, data._links); + this.addToObjectCache(paginatedList, request, data, alternativeURL); + return paginatedList; + } + + protected processArray(data: any, request: RestRequest): ObjectDomain[] { + let array: ObjectDomain[] = []; + data.forEach((datum) => { + array = [...array, this.process(datum, request)]; + } + ); + return array; + } + + protected deserialize(obj): any { + const type = obj.type; + const objConstructor = this.getConstructorFor(type); + if (hasValue(objConstructor)) { + const serializer = new this.serializerConstructor(objConstructor); + return serializer.deserialize(obj); + } else { + console.warn('cannot deserialize type ' + type); + return null; + } + } + + /** + * Returns the constructor for the given type, or null if there isn't a registered model for that + * type + * + * @param type the object to find the constructor for. + * @protected + */ + protected getConstructorFor(type: string): GenericConstructor { + if (hasValue(type)) { + return getClassForType(type) as GenericConstructor; + } else { + return null; + } + } + + /** + * Add the given object to the object cache + * + * @param co the {@link CacheableObject} to add + * @param request the {@link RestRequest} that was sent to the backend + * @param data the (partial) response from the server + * @param alternativeURL an alternative url that can be used to retrieve the object + */ + addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { + if (!isCacheableObject(co)) { + const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; + let dataJSON: string; + if (hasValue(data._embedded)) { + dataJSON = JSON.stringify(Object.assign({}, data, { + _embedded: '...' + })); + } else { + dataJSON = JSON.stringify(data); + } + console.warn(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`); + return; + } + + if (alternativeURL === co._links.self.href) { + alternativeURL = undefined; + } + + this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, alternativeURL); + } + + processPageInfo(payload: any): PageInfo { + if (hasValue(payload.page)) { + const pageInfoObject = new DSpaceSerializer(PageInfo).deserialize(payload.page); + if (pageInfoObject.currentPage >= 0) { + Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); + } + return pageInfoObject; + } else { + return undefined; + } + } + + protected flattenSingleKeyObject(obj: any): any { + const keys = Object.keys(obj); + if (keys.length !== 1) { + throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`); + } + return obj[keys[0]]; + } + + protected isSuccessStatus(statusCode: number) { + return statusCode >= 200 && statusCode < 300; + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts index 5516e83c07..eeb4e86fac 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,27 +1,115 @@ -import { Inject, Injectable } from '@angular/core'; -import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ResponseParsingService } from './parsing.service'; +import { + DspaceRestResponseParsingService, + isCacheableObject +} from './dspace-rest-response-parsing.service'; +import { hasValue } from '../../shared/empty.util'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { GenericConstructor } from '../shared/generic-constructor'; import { RestRequest } from './request.models'; -import { isNotEmpty } from '../../shared/empty.util'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { ParsedResponse } from '../cache/response.models'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { environment } from '../../../environments/environment'; -@Injectable() -export class EndpointMapResponseParsingService implements ResponseParsingService { +/** + * ResponseParsingService able to deal with HAL Endpoints that are only needed as steps + * on the way when discovering the path to a HAL Resource, and aren't properly typed. + * + * When all endpoints are properly typed, it can be removed. + */ +export class EndpointMapResponseParsingService extends DspaceRestResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - const links = data.payload._links; - for (const link of Object.keys(links)) { - links[link] = links[link].href; + /** + * Parse an endpoint map response. + * + * More lenient than DspaceRestResponseParsingService, in that it also allows objects we don't + * have a constructor for so their _links section can still be used to discover the path to a HAL + * resource. + * + * @param request the request that was sent to the backend + * @param response the response returned by the backend + */ + parse(request: RestRequest, response: RawRestResponse): ParsedResponse { + try { + response = this.ensureSelfLink(request, response); + const processRequestDTO = this.process(response.payload, request); + + if (hasValue(processRequestDTO)) { + const type: string = processRequestDTO.type; + let objConstructor; + if (hasValue(type)) { + objConstructor = getClassForType(type); + } + + if (isCacheableObject(processRequestDTO) && hasValue(objConstructor)) { + return new ParsedResponse(response.statusCode, processRequestDTO._links.self); + } else { + return new ParsedResponse(response.statusCode, undefined, processRequestDTO); + } + } else { + return new ParsedResponse(response.statusCode); } - return new EndpointMapSuccessResponse(links, data.statusCode, data.statusText); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from root endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); + } catch (e) { + console.warn(`Couldn't parse endpoint request at ${request.href}`); + return new ParsedResponse(response.statusCode, undefined, { + _links: response.payload._links + }); } } + + /** + * Try to deserialize the object the way DspaceRestResponseParsingService does, but if + * it doesn't work, return the plain object stripped of its type, instead of throwing an error + * + * That way it can still be used to determine HAL links. + * + * @param obj the object to deserialize + * @protected + */ + protected deserialize(obj): any { + const type: string = obj.type; + if (hasValue(type)) { + const objConstructor = getClassForType(type) as GenericConstructor; + + if (hasValue(objConstructor)) { + const serializer = new this.serializerConstructor(objConstructor); + return serializer.deserialize(obj); + } + } + return obj; + } + + /** + * Add the given object to the object cache + * + * This differs from the version in DspaceRestResponseParsingService in that it has to take in + * to account deserialized objects that aren't properly typed. So it will only add objects to the + * cache if we can find a constructor for them + * + * @param co the {@link CacheableObject} to add + * @param request the {@link RestRequest} that was sent to the backend + * @param data the (partial) response from the server + * @param alternativeURL an alternative url that can be used to retrieve the object + */ + addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { + if (!isCacheableObject(co)) { + const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; + let dataJSON: string; + if (hasValue(data._embedded)) { + dataJSON = JSON.stringify(Object.assign({}, data, { + _embedded: '...' + })); + } else { + dataJSON = JSON.stringify(data); + } + console.warn(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`); + return; + } + + if (hasValue(this.getConstructorFor((co as any).type))) { + this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, alternativeURL); + } + } + } diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts index 87de69b935..ee394a34ac 100644 --- a/src/app/core/data/entity-type-data.service.ts +++ b/src/app/core/data/entity-type-data.service.ts @@ -13,7 +13,7 @@ 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'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { FindListOptions } from './request.models'; import { RequestService } from './request.service'; @@ -65,21 +65,25 @@ export class ItemTypeDataService { /** * 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 linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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, ...linksToFollow: Array>): Observable> { - return this.dataService.findByHref(href, ...linksToFollow); + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); } /** * Returns a list of observables of {@link RemoteData} of {@link ItemType}s, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link ItemType} - * @param href The url of the {@link ItemType} we want to retrieve - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/entity-type.service.ts b/src/app/core/data/entity-type.service.ts index 39d45dc0b3..f81adb3217 100644 --- a/src/app/core/data/entity-type.service.ts +++ b/src/app/core/data/entity-type.service.ts @@ -15,9 +15,9 @@ import { Observable } from 'rxjs/internal/Observable'; 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'; +import {PaginatedList} from './paginated-list.model'; import {ItemType} from '../shared/item-relationships/item-type.model'; -import {getRemoteDataPayload, getSucceededRemoteData} from '../shared/operators'; +import {getRemoteDataPayload, getFirstSucceededRemoteData} from '../shared/operators'; /** * Service handling all ItemType requests @@ -60,7 +60,7 @@ export class EntityTypeService extends DataService { isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable { return relationshipType.leftType.pipe( - getSucceededRemoteData(), + getFirstSucceededRemoteData(), getRemoteDataPayload(), map((leftType) => leftType.uuid === itemType.uuid), ); diff --git a/src/app/core/data/entries-response-parsing.service.ts b/src/app/core/data/entries-response-parsing.service.ts deleted file mode 100644 index 09ae8ae1c5..0000000000 --- a/src/app/core/data/entries-response-parsing.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isNotEmpty } from '../../shared/empty.util'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { GenericConstructor } from '../shared/generic-constructor'; - -/** - * An abstract class to extend, responsible for parsing data for an entries response - */ -export abstract class EntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected toCache = false; - - constructor( - protected objectCache: ObjectCacheService, - ) { - super(); - } - - /** - * Abstract method to implement that must return the dspace serializer Constructor to use during parse - */ - abstract getSerializerModel(): GenericConstructor; - - /** - * Parse response - * - * @param request - * @param data - */ - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload)) { - let entries = []; - if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceSerializer(this.getSerializerModel()); - entries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); - } - return new GenericSuccessResponse(entries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from browse endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); - } - } - -} diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 4c91ffd4f1..d71a9da849 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -1,17 +1,20 @@ import { RequestService } from './request.service'; import { EpersonRegistrationService } from './eperson-registration.service'; -import { RegistrationSuccessResponse, RestResponse } from '../cache/response.models'; +import { RestResponse } from '../cache/response.models'; import { RequestEntry } from './request.reducer'; import { cold } from 'jasmine-marbles'; import { PostRequest } from './request.models'; import { Registration } from '../shared/registration.model'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { of as observableOf } from 'rxjs/internal/observable/of'; describe('EpersonRegistrationService', () => { let service: EpersonRegistrationService; let requestService: RequestService; let halService: any; + let rdbService: any; const registration = new Registration(); registration.email = 'test@mail.org'; @@ -20,7 +23,10 @@ describe('EpersonRegistrationService', () => { registrationWithUser.email = 'test@mail.org'; registrationWithUser.user = 'test-uuid'; + let rd; + beforeEach(() => { + rd = createSuccessfulRemoteDataObject(registrationWithUser); halService = new HALEndpointServiceStub('rest-url'); requestService = jasmine.createSpyObj('requestService', { @@ -29,8 +35,12 @@ describe('EpersonRegistrationService', () => { getByUUID: cold('a', {a: Object.assign(new RequestEntry(), {response: new RestResponse(true, 200, 'Success')})}) }); + rdbService = jasmine.createSpyObj('rdbService', { + buildFromRequestUUID: observableOf(rd) + }); service = new EpersonRegistrationService( requestService, + rdbService, halService ); }); @@ -61,17 +71,11 @@ describe('EpersonRegistrationService', () => { const expected = service.registerEmail('test@mail.org'); expect(requestService.configure).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); - expect(expected).toBeObservable(cold('a', {a: new RestResponse(true, 200, 'Success')})); + expect(expected).toBeObservable(cold('(a|)', {a: rd})); }); }); describe('searchByToken', () => { - beforeEach(() => { - (requestService.getByUUID as jasmine.Spy).and.returnValue( - cold('a', - {a: Object.assign(new RequestEntry(), {response: new RegistrationSuccessResponse(registrationWithUser, 200, 'Success')})}) - ); - }); it('should return a registration corresponding to the provided token', () => { const expected = service.searchByToken('test-token'); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index caa6150711..15bdced8d0 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -6,11 +6,12 @@ import { Observable } from 'rxjs'; import { filter, find, map, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Registration } from '../shared/registration.model'; -import { filterSuccessfulResponses, getResponseFromEntry } from '../shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators'; import { ResponseParsingService } from './parsing.service'; import { GenericConstructor } from '../shared/generic-constructor'; import { RegistrationResponseParsingService } from './registration-response-parsing.service'; -import { RegistrationSuccessResponse } from '../cache/response.models'; +import { RemoteData } from './remote-data'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @Injectable( { @@ -27,6 +28,7 @@ export class EpersonRegistrationService { constructor( protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, ) { @@ -52,7 +54,7 @@ export class EpersonRegistrationService { * Register a new email address * @param email */ - registerEmail(email: string) { + registerEmail(email: string): Observable> { const registration = new Registration(); registration.email = email; @@ -68,8 +70,8 @@ export class EpersonRegistrationService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() + return this.rdbService.buildFromRequestUUID(requestId).pipe( + getFirstCompletedRemoteData() ); } @@ -95,10 +97,10 @@ export class EpersonRegistrationService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - filterSuccessfulResponses(), - map((restResponse: RegistrationSuccessResponse) => { - return Object.assign(new Registration(), {email: restResponse.registration.email, token: token, user: restResponse.registration.user}); + return this.rdbService.buildFromRequestUUID(requestId).pipe( + getFirstSucceededRemoteData(), + map((restResponse: RemoteData) => { + return Object.assign(new Registration(), {email: restResponse.payload.email, token: token, user: restResponse.payload.user}); }), take(1), ); diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index edc538a39b..e264a46757 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -16,10 +16,9 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { configureRequest } from '../shared/operators'; import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list'; +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 diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index 0a552365f6..4633edd8e2 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,25 +1,38 @@ import { Injectable } from '@angular/core'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { FacetConfigSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; +import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; +import { FacetConfigResponse } from '../../shared/search/facet-config-response.model'; @Injectable() -export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - toCache = false; - constructor( - protected objectCache: ObjectCacheService, - ) { super(); - } - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { +export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService { + parse(request: RestRequest, data: RawRestResponse): ParsedResponse { const config = data.payload._embedded.facets; const serializer = new DSpaceSerializer(SearchFilterConfig); - const facetConfig = serializer.deserializeArray(config); - return new FacetConfigSuccessResponse(facetConfig, data.statusCode, data.statusText); + const filters = serializer.deserializeArray(config); + + const _links = { + self: data.payload._links.self + }; + + // fill in the missing links section + filters.forEach((filterConfig: SearchFilterConfig) => { + _links[filterConfig.name] = { + href: filterConfig._links.self.href + } + }) + + const facetConfigResponse = Object.assign(new FacetConfigResponse(), { + filters, + _links + }); + + this.addToObjectCache(facetConfigResponse, request, data); + + return new ParsedResponse(data.statusCode, facetConfigResponse._links.self); } } diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts deleted file mode 100644 index 7845e44e57..0000000000 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from '@angular/core'; -import { FacetValue } from '../../shared/search/facet-value.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { FacetValueMap, FacetValueMapSuccessResponse, FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - toCache = false; - - constructor( - protected objectCache: ObjectCacheService, - ) { super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - - const payload = data.payload; - const facetMap: FacetValueMap = new FacetValueMap(); - - const serializer = new DSpaceSerializer(FacetValue); - payload._embedded.facets.map((facet) => { - const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); - const facetValues = serializer.deserializeArray(values); - const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, data.statusText, this.processPageInfo(data.payload)); - facetMap[facet.name] = valuesResponse; - }); - - return new FacetValueMapSuccessResponse(facetMap, data.statusCode, data.statusText); - } -} diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index b56bedd1bb..6b9e832685 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,27 +1,20 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { FacetValue } from '../../shared/search/facet-value.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; +import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; +import { FacetValues } from '../../shared/search/facet-values.model'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; @Injectable() -export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - toCache = false; - constructor( - protected objectCache: ObjectCacheService, - ) { super(); - } - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { +export class FacetValueResponseParsingService extends DspaceRestResponseParsingService { + parse(request: RestRequest, data: RawRestResponse): ParsedResponse { const payload = data.payload; - - const serializer = new DSpaceSerializer(FacetValue); - // const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); - - const facetValues = serializer.deserializeArray(payload._embedded.values); - return new FacetValueSuccessResponse(facetValues, data.statusCode, data.statusText, this.processPageInfo(data.payload)); + const facetValues = new DSpaceSerializer(FacetValues).deserialize(payload); + facetValues.pageInfo = this.processPageInfo(payload); + facetValues.page = new DSpaceSerializer(FacetValue).deserializeArray(payload._embedded.values); + this.addToObjectCache(facetValues, request, data); + return new ParsedResponse(data.statusCode, facetValues._links.self); } } diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts index 7db7c27c29..47eb249812 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts @@ -10,7 +10,7 @@ import { hasValue } from '../../../shared/empty.util'; import { RequestParam } from '../../cache/models/request-param.model'; import { Authorization } from '../../shared/authorization.model'; import { RemoteData } from '../remote-data'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { Feature } from '../../shared/feature.model'; @@ -69,7 +69,7 @@ describe('AuthorizationDataService', () => { }); it('should call searchBy with the site\'s url', () => { - expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self)); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self), true); }); }); @@ -79,7 +79,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)); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf), true); }); }); @@ -89,7 +89,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)); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf), true); }); }); @@ -99,7 +99,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)); + expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf), true); }); }); }); @@ -134,7 +134,7 @@ describe('AuthorizationDataService', () => { describe('when searchByObject returns a 401', () => { beforeEach(() => { - spyOn(service, 'searchByObject').and.returnValue(observableOf(new RemoteData(false, false, true, undefined, undefined, 401))); + spyOn(service, 'searchByObject').and.returnValue(createFailedRemoteDataObject$('Unauthorized', 401)); }); it('should return false', (done) => { diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index ad9b724040..26b3e4edf3 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -15,20 +15,18 @@ import { HttpClient } from '@angular/common/http'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; import { AuthService } from '../../auth/auth.service'; import { SiteDataService } from '../site-data.service'; -import { FindListOptions, FindListRequest } from '../request.models'; +import { FindListOptions } from '../request.models'; import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list'; -import { catchError, find, map, switchMap, tap } from 'rxjs/operators'; +import { PaginatedList } from '../paginated-list.model'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { RequestParam } from '../../cache/models/request-param.model'; import { AuthorizationSearchParams } from './authorization-search-params'; -import { - addSiteObjectUrlIfEmpty, - oneAuthorizationMatchesFeature -} from './authorization-utils'; +import { addSiteObjectUrlIfEmpty, oneAuthorizationMatchesFeature } from './authorization-utils'; import { FeatureID } from './feature-id'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; /** * A service to retrieve {@link Authorization}s from the REST API @@ -64,6 +62,7 @@ export class AuthorizationDataService extends DataService { */ isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable { return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, followLink('feature')).pipe( + getFirstCompletedRemoteData(), map((authorizationRD) => { if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) { return authorizationRD.payload.page; @@ -91,38 +90,11 @@ export class AuthorizationDataService extends DataService { 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), ...linksToFollow); + return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), true, ...linksToFollow); }) ); } - /** - * Make a new FindListRequest with given search method - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param linksToFollow The array of [[FollowLinkConfig]] - * @return {Observable>} - * Return an observable that emits response from the server - */ - searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - - return hrefObs.pipe( - find((href: string) => hasValue(href)), - tap((href: string) => { - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - - this.requestService.configure(request); - } - ), - switchMap((href) => this.requestService.getByHref(href)), - switchMap((href) => - this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> - ) - ); - } - /** * Create {@link FindListOptions} with {@link RequestParam}s containing a "uri", "feature" and/or "eperson" parameter * @param objectUrl Required parameter value to add to {@link RequestParam} "uri" diff --git a/src/app/core/data/feature-authorization/feature-data.service.ts b/src/app/core/data/feature-authorization/feature-data.service.ts index 8003b6c31d..12be6f8452 100644 --- a/src/app/core/data/feature-authorization/feature-data.service.ts +++ b/src/app/core/data/feature-authorization/feature-data.service.ts @@ -1,4 +1,3 @@ -import { Observable } from 'rxjs/internal/Observable'; import { Injectable } from '@angular/core'; import { FEATURE } from '../../shared/feature.resource-type'; import { dataService } from '../../cache/builders/build-decorators'; @@ -13,12 +12,6 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; -import { FindListOptions, FindListRequest } from '../request.models'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list'; -import { find, skipWhile, switchMap, tap } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; /** * A service to retrieve {@link Feature}s from the REST API @@ -40,33 +33,4 @@ export class FeatureDataService extends DataService { ) { super(); } - - /** - * Make a new FindListRequest with given search method - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param linksToFollow The array of [[FollowLinkConfig]] - * @return {Observable>} - * Return an observable that emits response from the server - */ - searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - - return hrefObs.pipe( - find((href: string) => hasValue(href)), - tap((href: string) => { - this.requestService.removeByHrefSubstring(href); - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - - this.requestService.configure(request); - } - ), - switchMap((href) => this.requestService.getByHref(href)), - skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), - switchMap((href) => - this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> - ) - ); - } } diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts index 1934afba27..73f81aeccd 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts @@ -3,7 +3,7 @@ import { getMockObjectCacheService } from '../../shared/mocks/object-cache.servi import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from './parsing.service'; import { GetRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { FilteredDiscoveryQueryResponse } from '../cache/response.models'; describe('FilteredDiscoveryPageResponseParsingService', () => { @@ -26,7 +26,7 @@ describe('FilteredDiscoveryPageResponseParsingService', () => { }, statusCode: 200, statusText: 'OK' - } as DSpaceRESTV2Response; + } as RawRestResponse; it('should return a FilteredDiscoveryQueryResponse containing the correct query', () => { const response = service.parse(request, mockResponse); diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts index 02ce102ca6..244e6f0f8b 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -1,13 +1,13 @@ import { Inject, Injectable } from '@angular/core'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { FilteredDiscoveryQueryResponse, RestResponse } from '../cache/response.models'; /** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a discovery query (string) + * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a discovery query (string) * wrapped in a FilteredDiscoveryQueryResponse */ @Injectable() @@ -22,10 +22,10 @@ export class FilteredDiscoveryPageResponseParsingService extends BaseResponsePar /** * Parses data from the REST API to a discovery query wrapped in a FilteredDiscoveryQueryResponse * @param {RestRequest} request - * @param {DSpaceRESTV2Response} data + * @param {RawRestResponse} data * @returns {RestResponse} */ - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + parse(request: RestRequest, data: RawRestResponse): RestResponse { const query = data.payload['discovery-query']; return new FilteredDiscoveryQueryResponse(query, data.statusCode, data.statusText); } diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 282a43ec61..5b7278632a 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -10,11 +10,11 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemDataService } from './item-data.service'; -import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; +import { DeleteRequest, FindListOptions, PostRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -24,9 +24,6 @@ describe('ItemDataService', () => { generateRequestId(): string { return scopeID; }, - configure(request: RestRequest) { - // Do nothing - }, getByHref(requestHref: string) { const responseCacheEntry = new RequestEntry(); responseCacheEntry.response = new RestResponse(true, 200, 'OK'); @@ -36,17 +33,15 @@ describe('ItemDataService', () => { // Do nothing } }) as RequestService; - const rdbService = jasmine.createSpyObj('rdbService', { - toRemoteDataObservable: observableOf({}) - }); + const rdbService = getMockRemoteDataBuildService(); + + const itemEndpoint = 'https://rest.api/core/items'; const store = {} as Store; const objectCache = {} as ObjectCacheService; - const halEndpointService = { - getEndpoint(linkPath: string): Observable { - return cold('a', { a: itemEndpoint }); - } - } as HALEndpointService; + const halEndpointService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(itemEndpoint) + }); const bundleService = jasmine.createSpyObj('bundleService', { findByHref: {} }); @@ -68,7 +63,6 @@ describe('ItemDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const itemEndpoint = 'https://rest.api/core/items'; const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`; function initMockBrowseService(isSuccessful: boolean) { @@ -124,56 +118,11 @@ describe('ItemDataService', () => { }); }); - describe('getItemWithdrawEndpoint', () => { - beforeEach(() => { - scheduler = getTestScheduler(); - service = initTestService(); - - }); - - it('should return the endpoint to withdraw and reinstate items', () => { - const result = service.getItemWithdrawEndpoint(scopeID); - const expected = cold('a', { a: ScopedItemEndpoint }); - - expect(result).toBeObservable(expected); - }); - - it('should setWithDrawn', () => { - const expected = new RestResponse(true, 200, 'OK'); - const result = service.setWithDrawn(scopeID, true); - result.subscribe((v) => expect(v).toEqual(expected)); - - }); - }); - - describe('getItemDiscoverableEndpoint', () => { - beforeEach(() => { - scheduler = getTestScheduler(); - service = initTestService(); - - }); - - it('should return the endpoint to make an item private or public', () => { - const result = service.getItemDiscoverableEndpoint(scopeID); - const expected = cold('a', { a: ScopedItemEndpoint }); - - expect(result).toBeObservable(expected); - }); - - it('should setDiscoverable', () => { - const expected = new RestResponse(true, 200, 'OK'); - const result = service.setDiscoverable(scopeID, false); - result.subscribe((v) => expect(v).toEqual(expected)); - - }); - }); - describe('removeMappingFromCollection', () => { let result; beforeEach(() => { service = initTestService(); - spyOn(requestService, 'configure'); result = service.removeMappingFromCollection('item-id', 'collection-id'); }); @@ -187,7 +136,6 @@ describe('ItemDataService', () => { beforeEach(() => { service = initTestService(); - spyOn(requestService, 'configure'); result = service.mapToCollection('item-id', 'collection-href'); }); @@ -207,12 +155,14 @@ describe('ItemDataService', () => { beforeEach(() => { service = initTestService(); - spyOn(requestService, 'configure'); result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id'); }); - it('should configure a POST request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); + it('should configure a POST request', (done) => { + result.subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); + done(); + }); }); }); @@ -223,12 +173,14 @@ describe('ItemDataService', () => { beforeEach(() => { service = initTestService(); - spyOn(requestService, 'configure'); result = service.createBundle(itemId, bundleName); }); - it('should configure a POST request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); + it('should configure a POST request', (done) => { + result.subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)); + done(); + }); }); }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 218abb2dee..09e6039270 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -9,42 +9,35 @@ import { BrowseService } from '../browse/browse.service'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { Collection } from '../shared/collection.model'; 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, - filterSuccessfulResponses, - getRequestFromRequestHref, getRequestFromRequestUUID, - getResponseFromEntry -} from '../shared/operators'; +import { configureRequest } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { DeleteRequest, FindListOptions, GetRequest, - MappedCollectionsRequest, - PatchRequest, PostRequest, PutRequest, RestRequest } from './request.models'; -import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { Bundle } from '../shared/bundle.model'; import { MetadataMap } from '../shared/metadata.models'; import { BundleDataService } from './bundle-data.service'; +import { Operation } from 'fast-json-patch'; +import { NoContent } from '../shared/NoContent.model'; @Injectable() @dataService(ITEM) @@ -101,14 +94,13 @@ export class ItemDataService extends DataService { * @param itemId The item's id * @param collectionId The collection's id */ - public removeMappingFromCollection(itemId: string, collectionId: string): Observable { + public removeMappingFromCollection(itemId: string, collectionId: string): Observable> { return this.getMappedCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), - switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)), - getResponseFromEntry() + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), ); } @@ -117,7 +109,7 @@ export class ItemDataService extends DataService { * @param itemId The item's id * @param collectionHref The collection's self link */ - public mapToCollection(itemId: string, collectionHref: string): Observable { + public mapToCollection(itemId: string, collectionHref: string): Observable> { return this.getMappedCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), @@ -129,8 +121,7 @@ export class ItemDataService extends DataService { return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); }), configureRequest(this.requestService), - switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)), - getResponseFromEntry() + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)) ); } @@ -139,90 +130,47 @@ export class ItemDataService extends DataService { * @param itemId The item's id */ public getMappedCollections(itemId: string): Observable>> { - const request$ = this.getMappedCollectionsEndpoint(itemId).pipe( + const href$ = this.getMappedCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), - distinctUntilChanged(), - map((endpointURL: string) => new MappedCollectionsRequest(this.requestService.generateRequestId(), endpointURL)), - configureRequest(this.requestService) + take(1) ); - const requestEntry$ = request$.pipe( - switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - const payload$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: GenericSuccessResponse>) => response.payload) - ); + href$.subscribe((href: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); - return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); - } - - /** - * Get the endpoint for item withdrawal and reinstatement - * @param itemId - */ - public getItemWithdrawEndpoint(itemId: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, itemId)) - ); - } - - /** - * Get the endpoint to make item private and public - * @param itemId - */ - public getItemDiscoverableEndpoint(itemId: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, itemId)) - ); + return this.rdbService.buildList(href$); } /** * Set the isWithdrawn state of an item to a specified state - * @param itemId + * @param item * @param withdrawn */ - public setWithDrawn(itemId: string, withdrawn: boolean) { - const patchOperation = [{ + public setWithDrawn(item: Item, withdrawn: boolean): Observable> { + + const patchOperation = { op: 'replace', path: '/withdrawn', value: withdrawn - }]; + } as Operation; this.requestService.removeByHrefSubstring('/discover'); - return this.getItemWithdrawEndpoint(itemId).pipe( - distinctUntilChanged(), - map((endpointURL: string) => - new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation) - ), - configureRequest(this.requestService), - map((request: RestRequest) => request.uuid), - getRequestFromRequestUUID(this.requestService), - filter((requestEntry: RequestEntry) => requestEntry.completed), - map((requestEntry: RequestEntry) => requestEntry.response) - ); + return this.patch(item, [patchOperation]); } /** * Set the isDiscoverable state of an item to a specified state - * @param itemId + * @param item * @param discoverable */ - public setDiscoverable(itemId: string, discoverable: boolean) { - const patchOperation = [{ + public setDiscoverable(item: Item, discoverable: boolean): Observable> { + const patchOperation = { op: 'replace', path: '/discoverable', value: discoverable - }]; + } as Operation; this.requestService.removeByHrefSubstring('/discover'); - return this.getItemDiscoverableEndpoint(itemId).pipe( - distinctUntilChanged(), - map((endpointURL: string) => - new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation) - ), - configureRequest(this.requestService), - map((request: RestRequest) => request.uuid), - getRequestFromRequestUUID(this.requestService), - filter((requestEntry: RequestEntry) => requestEntry.completed), - map((requestEntry: RequestEntry) => requestEntry.response) - ); + return this.patch(item, [patchOperation]); +; } /** @@ -280,19 +228,7 @@ export class ItemDataService extends DataService { this.requestService.configure(request); }); - const selfLink$ = this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: any) => { - if (isNotEmpty(response.resourceSelfLinks)) { - return response.resourceSelfLinks[0]; - } - }), - distinctUntilChanged() - ) as Observable; - - return selfLink$.pipe( - switchMap((selfLink: string) => this.bundleService.findByHref(selfLink)), - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** @@ -311,7 +247,7 @@ export class ItemDataService extends DataService { * @param itemId * @param collection */ - public moveToCollection(itemId: string, collection: Collection): Observable { + public moveToCollection(itemId: string, collection: Collection): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); @@ -328,10 +264,7 @@ export class ItemDataService extends DataService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** @@ -356,16 +289,7 @@ export class ItemDataService extends DataService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - getResponseFromEntry(), - map((response: any) => { - if (isNotEmpty(response.resourceSelfLinks)) { - return response.resourceSelfLinks[0]; - } - }), - switchMap((selfLink: string) => this.findByHref(selfLink)) - ); + return this.rdbService.buildFromRequestUUID(requestId); } /** diff --git a/src/app/core/data/item-template-data.service.spec.ts b/src/app/core/data/item-template-data.service.spec.ts index d5f3e4b6f6..f7a32b4f82 100644 --- a/src/app/core/data/item-template-data.service.spec.ts +++ b/src/app/core/data/item-template-data.service.spec.ts @@ -57,7 +57,7 @@ describe('ItemTemplateDataService', () => { addPatch(self, operations) { // Do nothing } - } as ObjectCacheService; + } as any; const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', {a: itemEndpoint}); diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index 4f26f47eee..93a67aa46f 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -19,7 +19,8 @@ import { CollectionDataService } from './collection-data.service'; import { switchMap, map } from 'rxjs/operators'; import { BundleDataService } from './bundle-data.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { RestResponse } from '../cache/response.models'; +import { NoContent } from '../shared/NoContent.model'; +import { hasValue } from '../../shared/empty.util'; import { Operation } from 'fast-json-patch'; /* tslint:disable:max-classes-per-file */ @@ -99,11 +100,13 @@ class DataServiceImpl extends ItemDataService { /** * Set the collection ID and send a find by ID request * @param collectionID - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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, ...linksToFollow: Array>): Observable> { + findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { this.setCollectionEndpoint(collectionID); - return super.findById(collectionID, ...linksToFollow); + return super.findById(collectionID, reRequestOnStale, ...linksToFollow); } /** @@ -123,7 +126,9 @@ class DataServiceImpl extends ItemDataService { */ deleteByCollectionID(item: Item, collectionID: string): Observable { this.setRegularEndpoint(); - return super.delete(item.uuid).pipe(map((response: RestResponse) => response.isSuccessful)); + return super.delete(item.uuid).pipe( + map((response: RemoteData) => hasValue(response) && response.hasSucceeded) + ); } } @@ -166,17 +171,19 @@ export class ItemTemplateDataService implements UpdateDataService { return this.dataService.update(object); } - patch(dso: Item, operations: Operation[]): Observable { + patch(dso: Item, operations: Operation[]): Observable> { return this.dataService.patch(dso, operations); } /** * Find an item template by collection ID * @param collectionID - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @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, ...linksToFollow: Array>): Observable> { - return this.dataService.findByCollectionID(collectionID, ...linksToFollow); + findByCollectionID(collectionID: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByCollectionID(collectionID, reRequestOnStale, ...linksToFollow); } /** @@ -197,3 +204,4 @@ export class ItemTemplateDataService implements UpdateDataService { return this.dataService.deleteByCollectionID(item, collectionID); } } +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/license-data.service.ts b/src/app/core/data/license-data.service.ts index 23637be596..13ceaab693 100644 --- a/src/app/core/data/license-data.service.ts +++ b/src/app/core/data/license-data.service.ts @@ -13,7 +13,7 @@ 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'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { FindListOptions } from './request.models'; import { RequestService } from './request.service'; @@ -65,21 +65,25 @@ export class LicenseDataService { /** * 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 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 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, ...linksToFollow: Array>): Observable> { - return this.dataService.findByHref(href, ...linksToFollow); + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); } /** * Returns a list of observables of {@link RemoteData} of {@link License}s, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link License} - * @param href The URL of object we want to retrieve - * @param 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 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, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + findByAllHref(href: string, reRequestOnStale = true, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index ffeb6f9128..81464e5cb1 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -3,7 +3,7 @@ import { ExternalSourceService } from './external-source.service'; import { SearchService } from '../shared/search/search.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createPaginatedList } from '../../shared/testing/utils.test'; -import { PaginatedList } from './paginated-list'; +import { buildPaginatedList } from './paginated-list.model'; import { PageInfo } from '../shared/page-info.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; @@ -43,7 +43,7 @@ describe('LookupRelationService', () => { function init() { externalSourceService = jasmine.createSpyObj('externalSourceService', { - getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) + getExternalSourceEntries: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) }); searchService = jasmine.createSpyObj('searchService', { search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)), diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 153fcf1c54..49b3d47c6b 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -4,7 +4,7 @@ import { concat, distinctUntilChanged, map, multicast, startWith, take, takeWhil import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from './paginated-list.model'; import { SearchResult } from '../../shared/search/search-result.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; diff --git a/src/app/core/data/mapped-collections-reponse-parsing.service.ts b/src/app/core/data/mapped-collections-reponse-parsing.service.ts deleted file mode 100644 index bf8ed036e3..0000000000 --- a/src/app/core/data/mapped-collections-reponse-parsing.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { PaginatedList } from './paginated-list'; -import { PageInfo } from '../shared/page-info.model'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; - -@Injectable() -/** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse - * containing a PaginatedList of mapped collections - */ -export class MappedCollectionsReponseParsingService implements ResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - if (payload._embedded && payload._embedded.mappedCollections) { - const mappedCollections = payload._embedded.mappedCollections; - // TODO: When the API supports it, change this to fetch a paginated list, instead of creating static one - // Reason: Pagination is currently not supported on the mappedCollections endpoint - const paginatedMappedCollections = new PaginatedList(Object.assign(new PageInfo(), { - elementsPerPage: mappedCollections.length, - totalElements: mappedCollections.length, - totalPages: 1, - currentPage: 1 - }), mappedCollections); - return new GenericSuccessResponse(paginatedMappedCollections, data.statusCode, data.statusText); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from mappedCollections endpoint'), data - ) - ); - } - } -} diff --git a/src/app/core/data/metadata-field-data.service.spec.ts b/src/app/core/data/metadata-field-data.service.spec.ts index be610e3d8c..b2e293936e 100644 --- a/src/app/core/data/metadata-field-data.service.spec.ts +++ b/src/app/core/data/metadata-field-data.service.spec.ts @@ -59,7 +59,7 @@ describe('MetadataFieldDataService', () => { const expectedOptions = Object.assign(new FindListOptions(), { searchParams: [new RequestParam('schema', schema.prefix)] }); - expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions); + expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true); }); }); diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index af2ab7c45c..38ce181564 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { hasValue } from '../../shared/empty.util'; import { dataService } from '../cache/builders/build-decorators'; import { DataService } from './data.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -46,15 +46,17 @@ export class MetadataFieldDataService extends DataService { /** * Find metadata fields belonging to a metadata schema - * @param schema The metadata schema to list fields for - * @param options The options info used to retrieve the fields - * @param 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 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 = {}, ...linksToFollow: Array>) { + findBySchema(schema: MetadataSchema, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>) { const optionsWithSchema = Object.assign(new FindListOptions(), options, { searchParams: [new RequestParam('schema', schema.prefix)] }); - return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow); + return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, reRequestOnStale, ...linksToFollow); } /** @@ -69,9 +71,10 @@ export class MetadataFieldDataService extends DataService { * schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value * if there's an exact match * @param options The options info used to retrieve the fields + * @param 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 = {}, ...linksToFollow: Array>): Observable>> { + searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { const optionParams = Object.assign(new FindListOptions(), options, { searchParams: [ new RequestParam('schema', hasValue(schema) ? schema : ''), @@ -81,7 +84,7 @@ export class MetadataFieldDataService extends DataService { new RequestParam('exactName', hasValue(exactName) ? exactName : '') ] }); - return this.searchBy(this.searchByFieldNameLinkPath, optionParams, ...linksToFollow); + return this.searchBy(this.searchByFieldNameLinkPath, optionParams, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/data/metadata-schema-data.service.spec.ts b/src/app/core/data/metadata-schema-data.service.spec.ts index bf73deecb7..f8c972bc04 100644 --- a/src/app/core/data/metadata-schema-data.service.spec.ts +++ b/src/app/core/data/metadata-schema-data.service.spec.ts @@ -8,7 +8,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { MetadataSchema } from '../metadata/metadata-schema.model'; import { CreateRequest, PutRequest } from './request.models'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; describe('MetadataSchemaDataService', () => { let metadataSchemaService: MetadataSchemaDataService; @@ -30,9 +30,7 @@ describe('MetadataSchemaDataService', () => { notificationsService = jasmine.createSpyObj('notificationsService', { error: {} }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: createSuccessfulRemoteDataObject$(undefined) - }); + rdbService = getMockRemoteDataBuildService(); metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService); } diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts index 062bafab46..ee20b154a5 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -1,20 +1,16 @@ import { Injectable } from '@angular/core'; -import { RestResponse, SearchSuccessResponse } from '../cache/response.models'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; +import { ParsedResponse } from '../cache/response.models'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { hasValue } from '../../shared/empty.util'; -import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; +import { SearchObjects } from '../../shared/search/search-objects.model'; import { MetadataMap, MetadataValue } from '../shared/metadata.models'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; @Injectable() -export class MyDSpaceResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { +export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingService { + parse(request: RestRequest, data: RawRestResponse): ParsedResponse { // fallback for unexpected empty response const emptyPayload = { _embedded: { @@ -41,12 +37,8 @@ export class MyDSpaceResponseParsingService implements ResponseParsingService { const dsoSelfLinks = payload._embedded.objects .filter((object) => hasValue(object._embedded)) .map((object) => object._embedded.indexableObject) - .map((dso) => this.dsoParser.parse(request, { - payload: dso, - statusCode: data.statusCode, - statusText: data.statusText - })) - .map((obj) => obj.resourceSelfLinks) + .map((dso) => this.process(dso, request)) + .map((obj) => obj._links.self.href) .reduce((combined, thisElement) => [...combined, ...thisElement], []); const objects = payload._embedded.objects @@ -57,8 +49,10 @@ export class MyDSpaceResponseParsingService implements ResponseParsingService { _embedded: this.filterEmbeddedObjects(object) })); payload.objects = objects; - const deserialized = new DSpaceSerializer(SearchQueryResponse).deserialize(payload); - return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload)); + const deserialized = new DSpaceSerializer(SearchObjects).deserialize(payload); + deserialized.pageInfo = this.processPageInfo(payload) + this.addToObjectCache(deserialized, request, data); + return new ParsedResponse(data.statusCode, deserialized._links.self); } protected filterEmbeddedObjects(object) { diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index ff0babdd14..13bbabb286 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -2,8 +2,8 @@ import {type} from '../../../shared/ngrx/type'; import {Action} from '@ngrx/store'; import {Identifiable} from './object-updates.reducer'; import {INotification} from '../../../shared/notifications/models/notification.model'; -import { InjectionToken } from '@angular/core'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; +import { GenericConstructor } from '../../shared/generic-constructor'; /** * The list of ObjectUpdatesAction type definitions @@ -41,7 +41,7 @@ export class InitializeFieldsAction implements Action { url: string, fields: Identifiable[], lastModified: Date, - patchOperationServiceToken?: InjectionToken + patchOperationService?: GenericConstructor }; /** @@ -51,15 +51,15 @@ export class InitializeFieldsAction implements Action { * the unique url of the page for which the fields are being initialized * @param fields The identifiable fields of which the updates are kept track of * @param lastModified The last modified date of the object that belongs to the page - * @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch + * @param patchOperationService A {@link PatchOperationService} used for creating a patch */ constructor( url: string, fields: Identifiable[], lastModified: Date, - patchOperationServiceToken?: InjectionToken + patchOperationService?: GenericConstructor ) { - this.payload = { url, fields, lastModified, patchOperationServiceToken }; + this.payload = { url, fields, lastModified, patchOperationService }; } } diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index 4a14e2e874..b822da7e15 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -4,12 +4,16 @@ import { DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, - ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction, - RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, - SetEditableFieldUpdateAction, SetValidFieldUpdateAction + ReinstateObjectUpdatesAction, + RemoveAllObjectUpdatesAction, + RemoveFieldUpdateAction, + RemoveObjectUpdatesAction, + SelectVirtualMetadataAction, + SetEditableFieldUpdateAction, + SetValidFieldUpdateAction } from './object-updates.actions'; import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer'; -import {Relationship} from '../../shared/item-relationships/relationship.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; class NullAction extends RemoveFieldUpdateAction { type = null; @@ -232,7 +236,7 @@ describe('objectUpdatesReducer', () => { fieldUpdates: {}, virtualMetadataSources: {}, lastModified: modDate, - patchOperationServiceToken: undefined + patchOperationService: undefined } }; const newState = objectUpdatesReducer(testState, action); diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 378131ecf8..f62c3e7c12 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -13,11 +13,11 @@ import { SelectVirtualMetadataAction, } from './object-updates.actions'; import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { Relationship} from '../../shared/item-relationships/relationship.model'; -import { InjectionToken } from '@angular/core'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; -import { Item} from '../../shared/item.model'; -import { RelationshipType} from '../../shared/item-relationships/relationship-type.model'; +import { Item } from '../../shared/item.model'; +import { RelationshipType } from '../../shared/item-relationships/relationship-type.model'; +import { GenericConstructor } from '../../shared/generic-constructor'; /** * Path where discarded objects are saved @@ -100,7 +100,7 @@ export interface ObjectUpdatesEntry { fieldUpdates: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; - patchOperationServiceToken?: InjectionToken; + patchOperationService?: GenericConstructor; } /** @@ -175,7 +175,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { const url: string = action.payload.url; const fields: Identifiable[] = action.payload.fields; const lastModifiedServer: Date = action.payload.lastModified; - const patchOperationServiceToken: InjectionToken = action.payload.patchOperationServiceToken; + const patchOperationService: GenericConstructor = action.payload.patchOperationService; const fieldStates = createInitialFieldStates(fields); const newPageState = Object.assign( {}, @@ -184,7 +184,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { { fieldUpdates: {} }, { virtualMetadataSources: {} }, { lastModified: lastModifiedServer }, - { patchOperationServiceToken } + { patchOperationService } ); return Object.assign({}, state, { [url]: newPageState }); } diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index ae73dc851f..ff9f1ae608 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -4,14 +4,17 @@ import { ObjectUpdatesService } from './object-updates.service'; import { DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, + InitializeFieldsAction, + ReinstateObjectUpdatesAction, + RemoveFieldUpdateAction, + SelectVirtualMetadataAction, SetEditableFieldUpdateAction } from './object-updates.actions'; import { of as observableOf } from 'rxjs'; import { Notification } from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; -import {Relationship} from '../../shared/item-relationships/relationship.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; import { Injector } from '@angular/core'; describe('ObjectUpdatesService', () => { @@ -32,7 +35,6 @@ describe('ObjectUpdatesService', () => { }; const modDate = new Date(2010, 2, 11); - const injectionToken = 'fake-injection-token'; let patchOperationService; let injector: Injector; @@ -43,14 +45,14 @@ describe('ObjectUpdatesService', () => { [identifiable3.uuid]: { editable: true, isNew: true, isValid: true }, }; - const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationServiceToken: injectionToken - }; - store = new Store(undefined, undefined, undefined); - spyOn(store, 'dispatch'); patchOperationService = jasmine.createSpyObj('patchOperationService', { fieldUpdatesToPatchOperations: [] }); + const objectEntry = { + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService + }; + store = new Store(undefined, undefined, undefined); + spyOn(store, 'dispatch'); injector = jasmine.createSpyObj('injector', { get: patchOperationService }); @@ -294,9 +296,9 @@ describe('ObjectUpdatesService', () => { result$ = service.createPatch(url); }); - it('should inject the service using the token stored in the entry', (done) => { + it('should inject the service stored in the entry', (done) => { result$.subscribe(() => { - expect(injector.get).toHaveBeenCalledWith(injectionToken); + expect(injector.get).toHaveBeenCalledWith(patchOperationService); done(); }); }); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 11c9df272d..0d27ccd7e6 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -1,4 +1,4 @@ -import { Injectable, InjectionToken, Injector } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; import { coreSelector } from '../../core.selectors'; @@ -24,10 +24,17 @@ import { SetValidFieldUpdateAction } from './object-updates.actions'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, + hasValueOperator +} from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { Operation } from 'fast-json-patch'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; +import { GenericConstructor } from '../../shared/generic-constructor'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -59,10 +66,10 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are being mapped * @param fields The initial fields for the page's object * @param lastModified The date the object was last modified - * @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch + * @param patchOperationService A {@link PatchOperationService} used for creating a patch */ - initialize(url, fields: Identifiable[], lastModified: Date, patchOperationServiceToken?: InjectionToken): void { - this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, patchOperationServiceToken)); + initialize(url, fields: Identifiable[], lastModified: Date, patchOperationService?: GenericConstructor): void { + this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, patchOperationService)); } /** @@ -129,7 +136,9 @@ export class ObjectUpdatesService { */ getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { const objectUpdates = this.getObjectEntry(url); - return objectUpdates.pipe(map((objectEntry) => { + return objectUpdates.pipe( + hasValueOperator(), + map((objectEntry) => { const fieldUpdates: FieldUpdates = {}; for (const object of initialFields) { let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; @@ -346,7 +355,7 @@ export class ObjectUpdatesService { /** * Create a patch from the current object-updates state - * The {@link ObjectUpdatesEntry} should contain a patchOperationServiceToken, in order to define how a patch should + * The {@link ObjectUpdatesEntry} should contain a patchOperationService, in order to define how a patch should * be created. If it doesn't, an empty patch will be returned. * @param url The URL of the page for which the patch should be created */ @@ -354,8 +363,8 @@ export class ObjectUpdatesService { return this.getObjectEntry(url).pipe( map((entry) => { let patch = []; - if (hasValue(entry.patchOperationServiceToken)) { - patch = this.injector.get(entry.patchOperationServiceToken).fieldUpdatesToPatchOperations(entry.fieldUpdates); + if (hasValue(entry.patchOperationService)) { + patch = this.injector.get(entry.patchOperationService).fieldUpdatesToPatchOperations(entry.fieldUpdates); } return patch; }) diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts index 3b590cf58c..e1c356f9f9 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts @@ -3,26 +3,20 @@ import { MetadatumViewModel } from '../../../shared/metadata.models'; import { FieldUpdates } from '../object-updates.reducer'; import { Operation } from 'fast-json-patch'; import { FieldChangeType } from '../object-updates.actions'; -import { InjectionToken } from '@angular/core'; +import { Injectable } from '@angular/core'; import { MetadataPatchOperation } from './operations/metadata/metadata-patch-operation.model'; import { hasValue } from '../../../../shared/empty.util'; import { MetadataPatchAddOperation } from './operations/metadata/metadata-patch-add-operation.model'; import { MetadataPatchRemoveOperation } from './operations/metadata/metadata-patch-remove-operation.model'; import { MetadataPatchReplaceOperation } from './operations/metadata/metadata-patch-replace-operation.model'; -/** - * Token to use for injecting this service anywhere you want - * This token can used to store in the object-updates store - */ -export const METADATA_PATCH_OPERATION_SERVICE_TOKEN = new InjectionToken('MetadataPatchOperationService', { - providedIn: 'root', - factory: () => new MetadataPatchOperationService(), -}); - /** * Service transforming {@link FieldUpdates} into {@link Operation}s for metadata values * This expects the fields within every {@link FieldUpdate} to be {@link MetadatumViewModel}s */ +@Injectable({ + providedIn: 'root' +}) export class MetadataPatchOperationService implements PatchOperationService { /** diff --git a/src/app/core/data/paginated-list.model.ts b/src/app/core/data/paginated-list.model.ts new file mode 100644 index 0000000000..4f9b4c4a76 --- /dev/null +++ b/src/app/core/data/paginated-list.model.ts @@ -0,0 +1,199 @@ +import { PageInfo } from '../shared/page-info.model'; +import { hasValue, isEmpty, hasNoValue, isUndefined } from '../../shared/empty.util'; +import { HALResource } from '../shared/hal-resource.model'; +import { HALLink } from '../shared/hal-link.model'; +import { typedObject } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { PAGINATED_LIST } from './paginated-list.resource-type'; +import { ResourceType } from '../shared/resource-type'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { autoserialize, deserialize } from 'cerialize'; + +/** + * Factory function for a paginated list + * + * @param pageInfo The PageInfo for the new PaginatedList + * @param page The list of objects on the current page + * @param normalized Set to true if the list should only contain the links to the page objects, + * not the objects themselves + * @param _links Optional HALLinks to attach to the new PaginatedList + */ +export const buildPaginatedList = (pageInfo: PageInfo, page: T[], normalized = false, _links?: { [k: string]: HALLink | HALLink[] }): PaginatedList => { + const result = new PaginatedList(); + + if (hasNoValue(pageInfo)) { + pageInfo = new PageInfo(); + } + + result.pageInfo = pageInfo; + + let pageLinks: HALLink[]; + if (isEmpty(page)) { + pageLinks = []; + } else { + pageLinks = page.map((element: any) => { + if (hasValue(element) && hasValue(element._links) && hasValue(element._links.self)) { + return (element as HALResource)._links.self; + } else { + return null; + } + }) + // if none of the objects in page are HALResources, don't set a page link + if (pageLinks.every((link: HALLink) => hasNoValue(link))) { + pageLinks = undefined; + } + } + + result._links = Object.assign({}, _links, pageInfo._links, { + page: pageLinks + }); + + if (!normalized || isUndefined(pageLinks)) { + result.page = page; + } + + return result; +}; + +@typedObject +export class PaginatedList extends CacheableObject { + + static type = PAGINATED_LIST; + + /** + * The type of the list + */ + @excludeFromEquals + type = PAGINATED_LIST; + + /** + * The type of objects in the list + */ + @autoserialize + objectType?: ResourceType + + /** + * The list of objects that represents the current page + */ + page?: T[]; + + /** + * the {@link PageInfo} object + */ + @autoserialize + pageInfo?: PageInfo; + + /** + * The {@link HALLink}s for this PaginatedList + */ + @deserialize + _links: { + self: HALLink; + page: HALLink[]; + first?: HALLink; + prev?: HALLink; + next?: HALLink; + last?: HALLink; + }; + + get elementsPerPage(): number { + if (hasValue(this.pageInfo) && hasValue(this.pageInfo.elementsPerPage)) { + return this.pageInfo.elementsPerPage; + } + return this.getPageLength(); + } + + set elementsPerPage(value: number) { + this.pageInfo.elementsPerPage = value; + } + + get totalElements(): number { + if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) { + return this.pageInfo.totalElements; + } + return this.getPageLength(); + } + + set totalElements(value: number) { + this.pageInfo.totalElements = value; + } + + get totalPages(): number { + if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalPages)) { + return this.pageInfo.totalPages; + } + return 1; + } + + set totalPages(value: number) { + this.pageInfo.totalPages = value; + } + + get currentPage(): number { + if (hasValue(this.pageInfo) && hasValue(this.pageInfo.currentPage)) { + return this.pageInfo.currentPage; + } + return 1; + } + + set currentPage(value: number) { + this.pageInfo.currentPage = value; + } + + get first(): string { + if (hasValue(this._links.first) && hasValue(this._links.first.href)) { + return this._links.first.href; + } + } + + set first(first: string) { + this._links.first = { href: first }; + this.pageInfo._links.first = { href: first }; + } + + get prev(): string { + if (hasValue(this._links.prev) && hasValue(this._links.prev.href)) { + return this._links.prev.href; + } + } + + set prev(prev: string) { + this._links.prev = { href: prev }; + this.pageInfo._links.prev = { href: prev }; + } + + get next(): string { + if (hasValue(this._links.next) && hasValue(this._links.next.href)) { + return this._links.next.href; + } + } + + set next(next: string) { + this._links.next = { href: next }; + this.pageInfo._links.next = { href: next }; + } + + get last(): string { + if (hasValue(this._links.last) && hasValue(this._links.last.href)) { + return this._links.last.href; + } + } + + set last(last: string) { + this._links.last = { href: last }; + this.pageInfo._links.last = { href: last }; + } + + get self(): string { + return this._links.self.href; + } + + set self(self: string) { + this._links.self = { href: self }; + this.pageInfo._links.self = { href: self }; + } + + protected getPageLength() { + return (Array.isArray(this.page)) ? this.page.length : 0; + } +} diff --git a/src/app/core/data/paginated-list.resource-type.ts b/src/app/core/data/paginated-list.resource-type.ts new file mode 100644 index 0000000000..e58b6cc95d --- /dev/null +++ b/src/app/core/data/paginated-list.resource-type.ts @@ -0,0 +1,3 @@ +import { ResourceType } from '../shared/resource-type'; + +export const PAGINATED_LIST = new ResourceType('paginated-list'); diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts deleted file mode 100644 index 9f05ca7889..0000000000 --- a/src/app/core/data/paginated-list.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { hasValue } from '../../shared/empty.util'; - -export class PaginatedList { - - constructor(public pageInfo: PageInfo, - public page: T[]) { - } - - get elementsPerPage(): number { - if (hasValue(this.pageInfo) && hasValue(this.pageInfo.elementsPerPage)) { - return this.pageInfo.elementsPerPage; - } - return this.getPageLength(); - } - - set elementsPerPage(value: number) { - this.pageInfo.elementsPerPage = value; - } - - get totalElements(): number { - if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) { - return this.pageInfo.totalElements; - } - return this.getPageLength(); - } - - set totalElements(value: number) { - this.pageInfo.totalElements = value; - } - - get totalPages(): number { - if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalPages)) { - return this.pageInfo.totalPages; - } - return 1; - } - - set totalPages(value: number) { - this.pageInfo.totalPages = value; - } - - get currentPage(): number { - if (hasValue(this.pageInfo) && hasValue(this.pageInfo.currentPage)) { - return this.pageInfo.currentPage; - } - return 1; - } - - set currentPage(value: number) { - this.pageInfo.currentPage = value; - } - - get first(): string { - return this.pageInfo.first; - } - - set first(first: string) { - this.pageInfo._links.first = { href: first }; - } - - get prev(): string { - return this.pageInfo.prev; - } - set prev(prev: string) { - this.pageInfo._links.prev = { href: prev }; - } - - get next(): string { - return this.pageInfo.next; - } - - set next(next: string) { - this.pageInfo._links.next = { href: next }; - } - - get last(): string { - return this.pageInfo.last; - } - - set last(last: string) { - this.pageInfo._links.last = { href: last }; - } - - get self(): string { - return this.pageInfo.self; - } - - set self(self: string) { - this.pageInfo._links.self = { href: self }; - } - - protected getPageLength() { - return (Array.isArray(this.page)) ? this.page.length : 0; - } -} diff --git a/src/app/core/data/parsing.service.ts b/src/app/core/data/parsing.service.ts index ea8d1ea810..bebbd63fd7 100644 --- a/src/app/core/data/parsing.service.ts +++ b/src/app/core/data/parsing.service.ts @@ -1,7 +1,7 @@ -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RestRequest } from './request.models'; -import { RestResponse } from '../cache/response.models'; +import { ParsedResponse } from '../cache/response.models'; export interface ResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse; + parse(request: RestRequest, data: RawRestResponse): ParsedResponse; } diff --git a/src/app/core/data/process-files-response-parsing.service.ts b/src/app/core/data/process-files-response-parsing.service.ts deleted file mode 100644 index 0fa7c66869..0000000000 --- a/src/app/core/data/process-files-response-parsing.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; -import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { PaginatedList } from './paginated-list'; -import { PageInfo } from '../shared/page-info.model'; -import { Injectable } from '@angular/core'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { Bitstream } from '../shared/bitstream.model'; - -@Injectable() -/** - * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse - * containing a PaginatedList of a process's output files - */ -export class ProcessFilesResponseParsingService implements ResponseParsingService { - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - let page; - if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) { - const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]); - - if (isNotEmpty(bitstreams)) { - page = new PaginatedList(Object.assign(new PageInfo(), { - elementsPerPage: bitstreams.length, - totalElements: bitstreams.length, - totalPages: 1, - currentPage: 1 - }), bitstreams); - } - } - - if (isEmpty(page)) { - page = new PaginatedList(new PageInfo(), []); - } - - return new GenericSuccessResponse(page, data.statusCode, data.statusText); - } -} diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 48c1d502cc..687ed8c1e4 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -13,13 +13,12 @@ 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/internal/Observable'; -import { map, switchMap } from 'rxjs/operators'; -import { ProcessFilesRequest, RestRequest } from '../request.models'; -import { configureRequest, filterSuccessfulResponses } from '../../shared/operators'; -import { GenericSuccessResponse } from '../../cache/response.models'; -import { PaginatedList } from '../paginated-list'; +import { switchMap, take } from 'rxjs/operators'; +import { GetRequest } from '../request.models'; +import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; import { RemoteData } from '../remote-data'; +import { isNotEmptyOperator } from '../../../shared/empty.util'; @Injectable() @dataService(PROCESS) @@ -53,18 +52,16 @@ export class ProcessDataService extends DataService { * @param processId The ID of the process */ getFiles(processId: string): Observable>> { - const request$ = this.getFilesEndpoint(processId).pipe( - map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)), - configureRequest(this.requestService) - ); - const requestEntry$ = request$.pipe( - switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - const payload$ = requestEntry$.pipe( - filterSuccessfulResponses(), - map((response: GenericSuccessResponse>) => response.payload) + const href$ = this.getFilesEndpoint(processId).pipe( + isNotEmptyOperator(), + take(1) ); - return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); + href$.subscribe((href: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(href$); } } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index cecfeabf18..911e2d0890 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -10,16 +10,15 @@ import { HttpClient } from '@angular/common/http'; import { DefaultChangeAnalyzer } from '../default-change-analyzer.service'; import { Script } from '../../../process-page/scripts/script.model'; import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; -import { find, map, switchMap } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; import { RemoteData } from '../remote-data'; import { MultipartPostRequest, RestRequest } from '../request.models'; import { RequestService } from '../request.service'; import { Observable } from 'rxjs'; -import { RequestEntry } from '../request.reducer'; import { dataService } from '../../cache/builders/build-decorators'; import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; -import { hasValue } from '../../../shared/empty.util'; +import { Process } from '../../../process-page/processes/process.model'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; @@ -41,18 +40,18 @@ export class ScriptDataService extends DataService