Merge branch 'followlink-refactor' into metadata-and-relationships-combined-in-submission

This commit is contained in:
Art Lowel
2020-02-24 14:59:28 +01:00
347 changed files with 5892 additions and 4780 deletions

View File

@@ -1,4 +1,4 @@
import { browser, element, by, protractor } from 'protractor'; import { browser, by, element, protractor } from 'protractor';
import { promise } from 'selenium-webdriver'; import { promise } from 'selenium-webdriver';
export class ProtractorPage { export class ProtractorPage {

View File

@@ -134,7 +134,7 @@
"ngx-sortablejs": "^3.1.4", "ngx-sortablejs": "^3.1.4",
"nouislider": "^11.0.0", "nouislider": "^11.0.0",
"pem": "1.13.2", "pem": "1.13.2",
"reflect-metadata": "0.1.12", "reflect-metadata": "^0.1.13",
"rxjs": "6.5.4", "rxjs": "6.5.4",
"rxjs-spy": "^7.5.1", "rxjs-spy": "^7.5.1",
"sass-resources-loader": "^2.0.0", "sass-resources-loader": "^2.0.0",

View File

@@ -1,19 +1,18 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Router } from '@angular/router';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterStub } from '../../../../shared/testing/router-stub'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
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 { 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';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { RouterStub } from '../../../../shared/testing/router-stub';
import { RestResponse } from '../../../../core/cache/response.models';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
import { ResourceType } from '../../../../core/shared/resource-type';
import { AddBitstreamFormatComponent } from './add-bitstream-format.component'; import { AddBitstreamFormatComponent } from './add-bitstream-format.component';
describe('AddBitstreamFormatComponent', () => { describe('AddBitstreamFormatComponent', () => {

View File

@@ -1,21 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterStub } from '../../../../shared/testing/router-stub'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
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 { 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 { RemoteData } from '../../../../core/data/remote-data';
import { EditBitstreamFormatComponent } from './edit-bitstream-format.component'; import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; import { RouterStub } from '../../../../shared/testing/router-stub';
import { RestResponse } from '../../../../core/cache/response.models'; import { EditBitstreamFormatComponent } from './edit-bitstream-format.component';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
import { ResourceType } from '../../../../core/shared/resource-type';
describe('EditBitstreamFormatComponent', () => { describe('EditBitstreamFormatComponent', () => {
let comp: EditBitstreamFormatComponent; let comp: EditBitstreamFormatComponent;

View File

@@ -26,13 +26,21 @@ describe('MetadataRegistryComponent', () => {
const mockSchemasList = [ const mockSchemasList = [
{ {
id: 1, id: 1,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1'
},
},
prefix: 'dc', prefix: 'dc',
namespace: 'http://dublincore.org/documents/dcmi-terms/' namespace: 'http://dublincore.org/documents/dcmi-terms/'
}, },
{ {
id: 2, id: 2,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2'
},
},
prefix: 'mock', prefix: 'mock',
namespace: 'http://dspace.org/mockschema' namespace: 'http://dspace.org/mockschema'
} }

View File

@@ -20,7 +20,11 @@ class NullAction extends MetadataRegistryEditSchemaAction {
const schema: MetadataSchema = Object.assign(new MetadataSchema(), const schema: MetadataSchema = Object.assign(new MetadataSchema(),
{ {
id: 'schema-id', id: 'schema-id',
self: 'http://rest.self/schema/dc', _links: {
self: {
href: 'http://rest.self/schema/dc'
},
},
prefix: 'dc', prefix: 'dc',
namespace: 'http://dublincore.org/documents/dcmi-terms/' namespace: 'http://dublincore.org/documents/dcmi-terms/'
}); });
@@ -28,7 +32,11 @@ const schema: MetadataSchema = Object.assign(new MetadataSchema(),
const schema2: MetadataSchema = Object.assign(new MetadataSchema(), const schema2: MetadataSchema = Object.assign(new MetadataSchema(),
{ {
id: 'another-schema-id', id: 'another-schema-id',
self: 'http://rest.self/schema/dcterms', _links: {
self: {
href: 'http://rest.self/schema/dcterms',
},
},
prefix: 'dcterms', prefix: 'dcterms',
namespace: 'http://purl.org/dc/terms/' namespace: 'http://purl.org/dc/terms/'
}); });
@@ -36,7 +44,11 @@ const schema2: MetadataSchema = Object.assign(new MetadataSchema(),
const field: MetadataField = Object.assign(new MetadataField(), const field: MetadataField = Object.assign(new MetadataField(),
{ {
id: 'author-field-id', id: 'author-field-id',
self: 'http://rest.self/field/author', _links: {
self: {
href: 'http://rest.self/field/author',
},
},
element: 'contributor', element: 'contributor',
qualifier: 'author', qualifier: 'author',
scopeNote: 'Author of an item', scopeNote: 'Author of an item',
@@ -46,7 +58,11 @@ const field: MetadataField = Object.assign(new MetadataField(),
const field2: MetadataField = Object.assign(new MetadataField(), const field2: MetadataField = Object.assign(new MetadataField(),
{ {
id: 'title-field-id', id: 'title-field-id',
self: 'http://rest.self/field/title', _links: {
self: {
href: 'http://rest.self/field/title',
},
},
element: 'title', element: 'title',
qualifier: null, qualifier: null,
scopeNote: 'Title of an item', scopeNote: 'Title of an item',

View File

@@ -30,13 +30,21 @@ describe('MetadataSchemaComponent', () => {
const mockSchemasList = [ const mockSchemasList = [
{ {
id: 1, id: 1,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1',
},
},
prefix: 'dc', prefix: 'dc',
namespace: 'http://dublincore.org/documents/dcmi-terms/' namespace: 'http://dublincore.org/documents/dcmi-terms/'
}, },
{ {
id: 2, id: 2,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2',
},
},
prefix: 'mock', prefix: 'mock',
namespace: 'http://dspace.org/mockschema' namespace: 'http://dspace.org/mockschema'
} }
@@ -44,7 +52,11 @@ describe('MetadataSchemaComponent', () => {
const mockFieldsList = [ const mockFieldsList = [
{ {
id: 1, id: 1,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
},
},
element: 'contributor', element: 'contributor',
qualifier: 'advisor', qualifier: 'advisor',
scopeNote: null, scopeNote: null,
@@ -52,7 +64,11 @@ describe('MetadataSchemaComponent', () => {
}, },
{ {
id: 2, id: 2,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
},
},
element: 'contributor', element: 'contributor',
qualifier: 'author', qualifier: 'author',
scopeNote: null, scopeNote: null,
@@ -60,7 +76,11 @@ describe('MetadataSchemaComponent', () => {
}, },
{ {
id: 3, id: 3,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
},
},
element: 'contributor', element: 'contributor',
qualifier: 'editor', qualifier: 'editor',
scopeNote: 'test scope note', scopeNote: 'test scope note',
@@ -68,7 +88,11 @@ describe('MetadataSchemaComponent', () => {
}, },
{ {
id: 4, id: 4,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11', _links: {
self: {
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
},
},
element: 'contributor', element: 'contributor',
qualifier: 'illustrator', qualifier: 'illustrator',
scopeNote: null, scopeNote: null,

View File

@@ -1,3 +1,4 @@
import { filter, tap } from 'rxjs/operators';
import { CollectionItemMapperComponent } from './collection-item-mapper.component'; import { CollectionItemMapperComponent } from './collection-item-mapper.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
@@ -52,7 +53,12 @@ describe('CollectionItemMapperComponent', () => {
const mockCollection: Collection = Object.assign(new Collection(), { const mockCollection: Collection = Object.assign(new Collection(), {
id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4',
name: 'test-collection' name: 'test-collection',
_links: {
self: {
href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4'
}
}
}); });
const mockCollectionRD: RemoteData<Collection> = new RemoteData<Collection>(false, false, true, null, mockCollection); const mockCollectionRD: RemoteData<Collection> = new RemoteData<Collection>(false, false, true, null, mockCollection);
const mockSearchOptions = of(new PaginatedSearchOptions({ const mockSearchOptions = of(new PaginatedSearchOptions({

View File

@@ -22,6 +22,7 @@ import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.comp
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { SearchService } from '../../core/shared/search/search.service'; import { SearchService } from '../../core/shared/search/search.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-collection-item-mapper', selector: 'ds-collection-item-mapper',
@@ -122,7 +123,7 @@ export class CollectionItemMapperComponent implements OnInit {
if (shouldUpdate) { if (shouldUpdate) {
return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, {
sort: this.defaultSortOptions sort: this.defaultSortOptions
})) }),followLink('owningCollection'))
} }
}) })
); );
@@ -154,7 +155,7 @@ export class CollectionItemMapperComponent implements OnInit {
map((collectionRD: RemoteData<Collection>) => collectionRD.payload), map((collectionRD: RemoteData<Collection>) => collectionRD.payload),
switchMap((collection: Collection) => switchMap((collection: Collection) =>
observableCombineLatest(ids.map((id: string) => observableCombineLatest(ids.map((id: string) =>
remove ? this.itemDataService.removeMappingFromCollection(id, collection.id) : this.itemDataService.mapToCollection(id, collection.self) remove ? this.itemDataService.removeMappingFromCollection(id, collection.id) : this.itemDataService.mapToCollection(id, collection._links.self.href)
)) ))
) )
); );

View File

@@ -6,6 +6,7 @@ import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { find } from 'rxjs/operators'; import { find } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
/** /**
* This class represents a resolver that requests a specific collection before the route is activated * This class represents a resolver that requests a specific collection before the route is activated
@@ -23,7 +24,7 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id).pipe( return this.collectionService.findById(route.params.id, followLink('logo')).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded), find((RD) => hasValue(RD.error) || RD.hasSucceeded),
); );
} }

View File

@@ -0,0 +1,28 @@
import { of as observableOf } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommunityPageResolver } from './community-page.resolver';
describe('CommunityPageResolver', () => {
describe('resolve', () => {
let resolver: CommunityPageResolver;
let communityService: any;
const uuid = '1234-65487-12354-1235';
beforeEach(() => {
communityService = {
findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true })
};
resolver = new CommunityPageResolver(communityService);
});
it('should resolve a community with the correct id', () => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
.pipe(first())
.subscribe(
(resolved) => {
expect(resolved.payload.id).toEqual(uuid);
}
);
});
});
});

View File

@@ -6,6 +6,7 @@ import { Community } from '../core/shared/community.model';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { find } from 'rxjs/operators'; import { find } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
/** /**
* This class represents a resolver that requests a specific community before the route is activated * This class represents a resolver that requests a specific community before the route is activated
@@ -23,7 +24,12 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById(route.params.id).pipe( return this.communityService.findById(
route.params.id,
followLink('logo'),
followLink('subcommunities'),
followLink('collections')
).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded) find((RD) => hasValue(RD.error) || RD.hasSucceeded)
); );
} }

View File

@@ -1,15 +1,14 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { RouteService } from '../../core/services/route.service'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedModule } from '../../shared/shared.module'; import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { DeleteCommunityPageComponent } from './delete-community-page.component';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SharedModule } from '../../shared/shared.module';
import { DeleteCommunityPageComponent } from './delete-community-page.component';
describe('DeleteCommunityPageComponent', () => { describe('DeleteCommunityPageComponent', () => {
let comp: DeleteCommunityPageComponent; let comp: DeleteCommunityPageComponent;

View File

@@ -1,42 +1,43 @@
import { CommonModule } from '@angular/common';
import { EventEmitter } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonModule } from '@angular/common';
import { ItemCollectionMapperComponent } from './item-collection-mapper.component';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { RouterStub } from '../../../shared/testing/router-stub';
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
import { EventEmitter } from '@angular/core';
import { SearchServiceStub } from '../../../shared/testing/search-service-stub';
import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormsModule } from '@angular/forms';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { HostWindowService } from '../../../shared/host-window.service';
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
import { By } from '@angular/platform-browser';
import { Item } from '../../../core/shared/item.model';
import { ObjectSelectService } from '../../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub';
import { of } from 'rxjs/internal/observable/of'; import { of } from 'rxjs/internal/observable/of';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { ItemDataService } from '../../../core/data/item-data.service';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { PaginatedList } from '../../../core/data/paginated-list';
import { VarDirective } from '../../../shared/utils/var.directive'; import { RemoteData } from '../../../core/data/remote-data';
import { SearchFormComponent } from '../../../shared/search-form/search-form.component';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { ErrorComponent } from '../../../shared/error/error.component'; import { Item } from '../../../core/shared/item.model';
import { LoadingComponent } from '../../../shared/loading/loading.component'; import { PageInfo } from '../../../core/shared/page-info.model';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { ErrorComponent } from '../../../shared/error/error.component';
import { HostWindowService } from '../../../shared/host-window.service';
import { LoadingComponent } from '../../../shared/loading/loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component';
import { ObjectSelectService } from '../../../shared/object-select/object-select.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { SearchFormComponent } from '../../../shared/search-form/search-form.component';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub';
import { RouterStub } from '../../../shared/testing/router-stub';
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';
describe('ItemCollectionMapperComponent', () => { describe('ItemCollectionMapperComponent', () => {
let comp: ItemCollectionMapperComponent; let comp: ItemCollectionMapperComponent;
@@ -109,7 +110,8 @@ describe('ItemCollectionMapperComponent', () => {
{ provide: SearchService, useValue: searchServiceStub }, { provide: SearchService, useValue: searchServiceStub },
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub }, { provide: TranslateService, useValue: translateServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) } { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: CollectionDataService, useValue: {} }
] ]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -1,12 +1,18 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; import {
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload,
getSucceededRemoteData,
toDSpaceObjectListRD
} from '../../../core/shared/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, startWith, switchMap, take } from 'rxjs/operators'; import { map, startWith, switchMap, take } from 'rxjs/operators';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
@@ -81,6 +87,7 @@ export class ItemCollectionMapperComponent implements OnInit {
private searchService: SearchService, private searchService: SearchService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private itemDataService: ItemDataService, private itemDataService: ItemDataService,
private collectionDataService: CollectionDataService,
private translateService: TranslateService) { private translateService: TranslateService) {
} }
@@ -106,7 +113,8 @@ export class ItemCollectionMapperComponent implements OnInit {
); );
const owningCollectionRD$ = this.itemRD$.pipe( const owningCollectionRD$ = this.itemRD$.pipe(
switchMap((itemRD: RemoteData<Item>) => itemRD.payload.owningCollection) getFirstSucceededRemoteDataPayload(),
switchMap((item: Item) => this.collectionDataService.findOwningCollectionFor(item))
); );
const itemCollectionsAndOptions$ = observableCombineLatest( const itemCollectionsAndOptions$ = observableCombineLatest(
this.itemCollectionsRD$, this.itemCollectionsRD$,

View File

@@ -1,23 +1,22 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { EditInPlaceFieldComponent } from './edit-in-place-field.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistryService } from '../../../../core/registry/registry.service';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../../../shared/shared.module'; import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { TranslateModule } from '@ngx-translate/core'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
import { RegistryService } from '../../../../core/registry/registry.service';
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
import { SharedModule } from '../../../../shared/shared.module';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
let comp: EditInPlaceFieldComponent; let comp: EditInPlaceFieldComponent;
let fixture: ComponentFixture<EditInPlaceFieldComponent>; let fixture: ComponentFixture<EditInPlaceFieldComponent>;

View File

@@ -1,4 +1,5 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { LinkService } from '../../../core/cache/builders/link.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';

View File

@@ -1,23 +1,23 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Item } from '../../../core/shared/item.model';
import { RouterStub } from '../../../shared/testing/router-stub';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { ItemMoveComponent } from './item-move.component';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { of as observableOf } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
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 { 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 { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { RouterStub } from '../../../shared/testing/router-stub';
import { ItemMoveComponent } from './item-move.component';
describe('ItemMoveComponent', () => { describe('ItemMoveComponent', () => {
let comp: ItemMoveComponent; let comp: ItemMoveComponent;
@@ -50,16 +50,14 @@ describe('ItemMoveComponent', () => {
}) })
}; };
const collection1 = Object.assign(new Collection(),{ const collection1 = Object.assign(new Collection(), {
uuid: 'collection-uuid-1', uuid: 'collection-uuid-1',
name: 'Test collection 1', name: 'Test collection 1'
self: 'self-link-1',
}); });
const collection2 = Object.assign(new Collection(),{ const collection2 = Object.assign(new Collection(), {
uuid: 'collection-uuid-2', uuid: 'collection-uuid-2',
name: 'Test collection 2', name: 'Test collection 2'
self: 'self-link-2',
}); });
const mockSearchService = { const mockSearchService = {
@@ -80,23 +78,20 @@ describe('ItemMoveComponent', () => {
const notificationsServiceStub = new NotificationsServiceStub(); const notificationsServiceStub = new NotificationsServiceStub();
describe('ItemMoveComponent success', () => { describe('ItemMoveComponent success', () => {
beforeEach(async(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemMoveComponent], declarations: [ItemMoveComponent],
providers: [ providers: [
{provide: ActivatedRoute, useValue: routeStub}, { provide: ActivatedRoute, useValue: routeStub },
{provide: Router, useValue: routerStub}, { provide: Router, useValue: routerStub },
{provide: ItemDataService, useValue: mockItemDataService}, { provide: ItemDataService, useValue: mockItemDataService },
{provide: NotificationsService, useValue: notificationsServiceStub}, { provide: NotificationsService, useValue: notificationsServiceStub },
{provide: SearchService, useValue: mockSearchService}, { provide: SearchService, useValue: mockSearchService },
], schemas: [ ], schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
] ]
}).compileComponents(); }).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemMoveComponent); fixture = TestBed.createComponent(ItemMoveComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -141,23 +136,20 @@ describe('ItemMoveComponent', () => {
}); });
describe('ItemMoveComponent fail', () => { describe('ItemMoveComponent fail', () => {
beforeEach(async(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemMoveComponent], declarations: [ItemMoveComponent],
providers: [ providers: [
{provide: ActivatedRoute, useValue: routeStub}, { provide: ActivatedRoute, useValue: routeStub },
{provide: Router, useValue: routerStub}, { provide: Router, useValue: routerStub },
{provide: ItemDataService, useValue: mockItemDataServiceFail}, { provide: ItemDataService, useValue: mockItemDataServiceFail },
{provide: NotificationsService, useValue: notificationsServiceStub}, { provide: NotificationsService, useValue: notificationsServiceStub },
{provide: SearchService, useValue: mockSearchService}, { provide: SearchService, useValue: mockSearchService },
], schemas: [ ], schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
] ]
}).compileComponents(); }).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemMoveComponent); fixture = TestBed.createComponent(ItemMoveComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -1,19 +1,22 @@
import {EditRelationshipListComponent} from './edit-relationship-list.component'; import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; import { By } from '@angular/platform-browser';
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; import { TranslateModule } from '@ngx-translate/core';
import {of as observableOf} from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs/internal/observable/of';
import {RemoteData} from '../../../../core/data/remote-data'; import { LinkService } from '../../../../core/cache/builders/link.service';
import {Item} from '../../../../core/shared/item.model'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import {PaginatedList} from '../../../../core/data/paginated-list'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import {PageInfo} from '../../../../core/shared/page-info.model'; import { PaginatedList } from '../../../../core/data/paginated-list';
import {FieldChangeType} from '../../../../core/data/object-updates/object-updates.actions'; import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
import {SharedModule} from '../../../../shared/shared.module'; import { RemoteData } from '../../../../core/data/remote-data';
import {TranslateModule} from '@ngx-translate/core'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import {By} from '@angular/platform-browser'; import { Item } from '../../../../core/shared/item.model';
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { getMockLinkService } from '../../../../shared/mocks/mock-link-service';
import { SharedModule } from '../../../../shared/shared.module';
import { EditRelationshipListComponent } from './edit-relationship-list.component';
let comp: EditRelationshipListComponent; let comp: EditRelationshipListComponent;
let fixture: ComponentFixture<EditRelationshipListComponent>; let fixture: ComponentFixture<EditRelationshipListComponent>;
@@ -57,7 +60,11 @@ describe('EditRelationshipListComponent', () => {
}); });
relationship1 = Object.assign(new Relationship(), { relationship1 = Object.assign(new Relationship(), {
self: url + '/2', _links: {
self: {
href: url + '/2'
}
},
id: '2', id: '2',
uuid: '2', uuid: '2',
leftId: 'author1', leftId: 'author1',
@@ -68,7 +75,11 @@ describe('EditRelationshipListComponent', () => {
}); });
relationship2 = Object.assign(new Relationship(), { relationship2 = Object.assign(new Relationship(), {
self: url + '/3', _links: {
self: {
href: url + '/3'
}
},
id: '3', id: '3',
uuid: '3', uuid: '3',
leftId: 'author2', leftId: 'author2',
@@ -79,7 +90,9 @@ describe('EditRelationshipListComponent', () => {
}); });
item = Object.assign(new Item(), { item = Object.assign(new Item(), {
self: 'fake-item-url/publication', _links: {
self: { href: 'fake-item-url/publication' }
},
id: 'publication', id: 'publication',
uuid: 'publication', uuid: 'publication',
relationships: observableOf(new RemoteData( relationships: observableOf(new RemoteData(
@@ -142,6 +155,8 @@ describe('EditRelationshipListComponent', () => {
declarations: [EditRelationshipListComponent], declarations: [EditRelationshipListComponent],
providers: [ providers: [
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: RelationshipTypeService, useValue: {} },
{ provide: LinkService, useValue: getMockLinkService() },
], schemas: [ ], schemas: [
NO_ERRORS_SCHEMA NO_ERRORS_SCHEMA
] ]

View File

@@ -1,15 +1,21 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer'; import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer';
import {Item} from '../../../../core/shared/item.model'; import {Item} from '../../../../core/shared/item.model';
import {map, switchMap} from 'rxjs/operators'; import { map, switchMap, tap } from 'rxjs/operators';
import {hasValue} from '../../../../shared/empty.util'; import {hasValue} from '../../../../shared/empty.util';
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators'; import {
import {combineLatest as observableCombineLatest, combineLatest} from 'rxjs'; getAllSucceededRemoteData,
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model'; getRemoteDataPayload,
getSucceededRemoteData
} from '../../../../core/shared/operators';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-edit-relationship-list', selector: 'ds-edit-relationship-list',
@@ -47,6 +53,7 @@ export class EditRelationshipListComponent implements OnInit {
constructor( constructor(
protected objectUpdatesService: ObjectUpdatesService, protected objectUpdatesService: ObjectUpdatesService,
protected linkService: LinkService
) { ) {
} }
@@ -71,7 +78,7 @@ export class EditRelationshipListComponent implements OnInit {
*/ */
private getLabel(): Observable<string> { private getLabel(): Observable<string> {
return combineLatest([ return observableCombineLatest([
this.relationshipType.leftType, this.relationshipType.leftType,
this.relationshipType.rightType, this.relationshipType.rightType,
].map((itemTypeRD) => itemTypeRD.pipe( ].map((itemTypeRD) => itemTypeRD.pipe(
@@ -94,8 +101,20 @@ export class EditRelationshipListComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.updates$ = this.item.relationships.pipe( this.updates$ = this.item.relationships.pipe(
getAllSucceededRemoteData(),
map((relationships) => relationships.payload.page.filter((relationship) => relationship)), map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
switchMap((itemRelationships) => map((relationships: Relationship[]) =>
relationships.map((relationship: Relationship) => {
this.linkService.resolveLinks(
relationship,
followLink('relationshipType'),
followLink('leftItem'),
followLink('rightItem'),
);
return relationship;
})
),
switchMap((itemRelationships: Relationship[]) =>
observableCombineLatest( observableCombineLatest(
itemRelationships itemRelationships
.map((relationship) => relationship.relationshipType.pipe( .map((relationship) => relationship.relationshipType.pipe(

View File

@@ -1,16 +1,16 @@
import { async, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { TranslateModule } from '@ngx-translate/core';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { EditRelationshipComponent } from './edit-relationship.component'; import { async, TestBed } from '@angular/core/testing';
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 { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { EditRelationshipComponent } from './edit-relationship.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
let objectUpdatesService; let objectUpdatesService;
@@ -42,7 +42,11 @@ describe('EditRelationshipComponent', () => {
}); });
item = Object.assign(new Item(), { item = Object.assign(new Item(), {
self: 'fake-item-url/publication', _links: {
self: {
href: 'fake-item-url/publication'
}
},
id: 'publication', id: 'publication',
uuid: 'publication', uuid: 'publication',
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
@@ -54,7 +58,9 @@ describe('EditRelationshipComponent', () => {
relationships = [ relationships = [
Object.assign(new Relationship(), { Object.assign(new Relationship(), {
self: url + '/2', _links: {
self: { href: url + '/2' }
},
id: '2', id: '2',
uuid: '2', uuid: '2',
leftId: 'author1', leftId: 'author1',
@@ -64,7 +70,9 @@ describe('EditRelationshipComponent', () => {
rightItem: observableOf(new RemoteData(false, false, true, undefined, item)), rightItem: observableOf(new RemoteData(false, false, true, undefined, item)),
}), }),
Object.assign(new Relationship(), { Object.assign(new Relationship(), {
self: url + '/3', _links: {
self: { href: url + '/3' }
},
id: '3', id: '3',
uuid: '3', uuid: '3',
leftId: 'author2', leftId: 'author2',

View File

@@ -1,32 +1,35 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemRelationshipsComponent } from './item-relationships.component';
import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationType } from '../../../shared/notifications/models/notification-type';
import { RouterStub } from '../../../shared/testing/router-stub';
import { TestScheduler } from 'rxjs/testing';
import { SharedModule } from '../../../shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateModule } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { GLOBAL_CONFIG } from '../../../../config'; import { GLOBAL_CONFIG } from '../../../../config';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RestResponse } from '../../../core/cache/response.models';
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';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { NotificationType } from '../../../shared/notifications/models/notification-type';
import { RelationshipService } from '../../../core/data/relationship.service'; import {
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; INotification,
import { getTestScheduler } from 'jasmine-marbles'; Notification
import { RestResponse } from '../../../core/cache/response.models'; } from '../../../shared/notifications/models/notification.model';
import { RequestService } from '../../../core/data/request.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EntityTypeService } from '../../../core/data/entity-type.service'; import { SharedModule } from '../../../shared/shared.module';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { RouterStub } from '../../../shared/testing/router-stub';
import { ItemRelationshipsComponent } from './item-relationships.component';
let comp: any; let comp: any;
let fixture: ComponentFixture<ItemRelationshipsComponent>; let fixture: ComponentFixture<ItemRelationshipsComponent>;
@@ -77,13 +80,17 @@ describe('ItemRelationshipsComponent', () => {
relationships = [ relationships = [
Object.assign(new Relationship(), { Object.assign(new Relationship(), {
self: url + '/2', _links: {
self: { href: url + '/2' }
},
id: '2', id: '2',
uuid: '2', uuid: '2',
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
}), }),
Object.assign(new Relationship(), { Object.assign(new Relationship(), {
self: url + '/3', _links: {
self: { href: url + '/3' }
},
id: '3', id: '3',
uuid: '3', uuid: '3',
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
@@ -91,7 +98,9 @@ describe('ItemRelationshipsComponent', () => {
]; ];
item = Object.assign(new Item(), { item = Object.assign(new Item(), {
self: 'fake-item-url/publication', _links: {
self: { href: 'fake-item-url/publication' }
},
id: 'publication', id: 'publication',
uuid: 'publication', uuid: 'publication',
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))), relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))),

View File

@@ -4,6 +4,7 @@ import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/dat
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { zip as observableZip } from 'rxjs'; import { 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 { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
@@ -71,7 +72,10 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
super.ngOnInit(); super.ngOnInit();
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
filter((exists: boolean) => !exists), filter((exists: boolean) => !exists),
switchMap(() => this.itemService.findById(this.item.uuid)), switchMap(() => this.itemService.findById(this.item.uuid,
followLink('owningCollection'),
followLink('bundles'),
followLink('relationships'))),
getSucceededRemoteData(), getSucceededRemoteData(),
).subscribe((itemRD: RemoteData<Item>) => { ).subscribe((itemRD: RemoteData<Item>) => {
this.item = itemRD.payload; this.item = itemRD.payload;
@@ -94,7 +98,11 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
this.relationshipTypes$ = this.entityType$.pipe( this.relationshipTypes$ = this.entityType$.pipe(
switchMap((entityType) => switchMap((entityType) =>
this.entityTypeService.getEntityTypeRelationships(entityType.id).pipe( this.entityTypeService.getEntityTypeRelationships(
entityType.id,
followLink('leftType'),
followLink('rightType'))
.pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((relationshipTypes) => relationshipTypes.page), map((relationshipTypes) => relationshipTypes.page),

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper *ngIf="hasSucceeded() | async" [label]="label | translate"> <ds-metadata-field-wrapper *ngIf="(this.collectionsRD$ | async)?.hasSucceeded" [label]="label | translate">
<div class="collections"> <div class="collections">
<a *ngFor="let collection of (collections | async); let last=last;" [routerLink]="['/collections', collection.id]"> <a *ngFor="let collection of (this.collectionsRD$ | async)?.payload?.page; let last=last;" [routerLink]="['/collections', collection.id]">
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span> <span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</div> </div>

View File

@@ -1,22 +1,20 @@
import { CollectionsComponent } from './collections.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Collection } from '../../../core/shared/collection.model';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { getMockRemoteDataBuildService } from '../../../shared/mocks/mock-remote-data-build.service';
import { Item } from '../../../core/shared/item.model';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
createFailedRemoteDataObject$, import { CollectionDataService } from '../../../core/data/collection-data.service';
createSuccessfulRemoteDataObject$ import { Collection } from '../../../core/shared/collection.model';
} from '../../../shared/testing/utils'; import { Item } from '../../../core/shared/item.model';
import { getMockRemoteDataBuildService } from '../../../shared/mocks/mock-remote-data-build.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { CollectionsComponent } from './collections.component';
let collectionsComponent: CollectionsComponent; let collectionsComponent: CollectionsComponent;
let fixture: ComponentFixture<CollectionsComponent>; let fixture: ComponentFixture<CollectionsComponent>;
let collectionDataServiceStub;
const mockCollection1: Collection = Object.assign(new Collection(), { const mockCollection1: Collection = Object.assign(new Collection(), {
metadata: { metadata: {
'dc.description.abstract': [ 'dc.description.abstract': [
@@ -32,12 +30,22 @@ const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: cre
const failedMockItem: Item = Object.assign(new Item(), {owningCollection: createFailedRemoteDataObject$(mockCollection1)}); const failedMockItem: Item = Object.assign(new Item(), {owningCollection: createFailedRemoteDataObject$(mockCollection1)});
describe('CollectionsComponent', () => { describe('CollectionsComponent', () => {
collectionDataServiceStub = {
findOwningCollectionFor(item: Item) {
if (item === succeededMockItem) {
return createSuccessfulRemoteDataObject$(mockCollection1);
} else {
return createFailedRemoteDataObject$(mockCollection1);
}
}
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot()],
declarations: [ CollectionsComponent ], declarations: [ CollectionsComponent ],
providers: [ providers: [
{ provide: RemoteDataBuildService, useValue: getMockRemoteDataBuildService()} { provide: RemoteDataBuildService, useValue: getMockRemoteDataBuildService()},
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]

View File

@@ -1,12 +1,13 @@
import {map} from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; 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 { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { PageInfo } from '../../../core/shared/page-info.model';
import { RemoteData } from '../../../core/data/remote-data';
/** /**
* This component renders the parent collections section of the item * This component renders the parent collections section of the item
@@ -25,9 +26,9 @@ export class CollectionsComponent implements OnInit {
separator = '<br/>'; separator = '<br/>';
collections: Observable<Collection[]>; collectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>;
constructor(private rdbs: RemoteDataBuildService) { constructor(private cds: CollectionDataService) {
} }
@@ -37,11 +38,25 @@ export class CollectionsComponent implements OnInit {
// TODO: this should use parents, but the collections // TODO: this should use parents, but the collections
// for an Item aren't returned by the REST API yet, // for an Item aren't returned by the REST API yet,
// only the owning collection // only the owning collection
this.collections = this.item.owner.pipe(map((rd: RemoteData<Collection>) => [rd.payload])); this.collectionsRD$ = this.cds.findOwningCollectionFor(this.item).pipe(
map((rd: RemoteData<Collection>) => {
if (rd.hasSucceeded) {
return new RemoteData(
false,
false,
true,
undefined,
new PaginatedList({
elementsPerPage: 10,
totalPages: 1,
currentPage: 1,
totalElements: 1
} as PageInfo, [rd.payload])
);
} else {
return rd as any;
}
})
);
} }
hasSucceeded() {
return this.item.owner.pipe(map((rd: RemoteData<Collection>) => rd.hasSucceeded));
}
} }

View File

@@ -1,7 +1,7 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<div class="file-section row" *ngFor="let file of (bitstreamsObs | async); let last=last;"> <div class="file-section row" *ngFor="let file of (bitstreams$ | async); let last=last;">
<div class="col-3"> <div class="col-3">
<ds-thumbnail [thumbnail]="thumbnails.get(file.id) | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
</div> </div>
<div class="col-7"> <div class="col-7">
<dl class="row"> <dl class="row">
@@ -21,7 +21,7 @@
</dl> </dl>
</div> </div>
<div class="col-2"> <div class="col-2">
<a [href]="file.content" [download]="file.name"> <a [href]="file._links.content.href" [download]="file.name">
{{"item.page.filesection.download" | translate}} {{"item.page.filesection.download" | translate}}
</a> </a>
</div> </div>

View File

@@ -1,10 +1,13 @@
import { Component, Injector, Input, OnInit } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Component, Input, OnInit } from '@angular/core'; import { map, startWith } from 'rxjs/operators';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
import { map } from 'rxjs/operators';
/** /**
* This component renders the file section of the item * This component renders the file section of the item
@@ -22,27 +25,49 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
label: string; label: string;
bitstreamsObs: Observable<Bitstream[]>; bitstreams$: Observable<Bitstream[]>;
thumbnails: Map<string, Observable<Bitstream>> = new Map(); constructor(
bitstreamDataService: BitstreamDataService
) {
super(bitstreamDataService);
}
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
} }
initialize(): void { initialize(): void {
const originals = this.item.getFiles(); // TODO pagination
const licenses = this.item.getBitstreamsByBundleName('LICENSE'); const originals$ = this.bitstreamDataService.findAllByItemAndBundleName(
this.bitstreamsObs = observableCombineLatest(originals, licenses).pipe(map(([o, l]) => [...o, ...l])); this.item,
this.bitstreamsObs.subscribe( 'ORIGINAL',
(files) => { elementsPerPage: Number.MAX_SAFE_INTEGER },
files.forEach( followLink( 'format')
).pipe(
getFirstSucceededRemoteListPayload(),
startWith([])
);
const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName(
this.item,
'LICENSE',
{ elementsPerPage: Number.MAX_SAFE_INTEGER },
followLink( 'format')
).pipe(
getFirstSucceededRemoteListPayload(),
startWith([])
);
this.bitstreams$ = observableCombineLatest(originals$, licenses$).pipe(
map(([o, l]) => [...o, ...l]),
map((files: Bitstream[]) =>
files.map(
(original) => { (original) => {
const thumbnail: Observable<Bitstream> = this.item.getThumbnailForOriginal(original); original.thumbnail = this.bitstreamDataService.getMatchingThumbnail(this.item, original);
this.thumbnails.set(original.id, thumbnail); return original;
} }
) )
) )
);
} }
} }

View File

@@ -6,6 +6,7 @@ import { ItemDataService } from '../core/data/item-data.service';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { find } from 'rxjs/operators'; import { find } from 'rxjs/operators';
import { followLink } from '../shared/utils/follow-link-config.model';
/** /**
* This class represents a resolver that requests a specific item before the route is activated * This class represents a resolver that requests a specific item before the route is activated
@@ -23,9 +24,12 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id) return this.itemService.findById(route.params.id,
.pipe( followLink('owningCollection'),
find((RD) => hasValue(RD.error) || RD.hasSucceeded), followLink('bundles'),
); followLink('relationships')
).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);
} }
} }

View File

@@ -1,7 +1,7 @@
<ng-container *ngVar="(bitstreamsObs | async) as bitstreams"> <ng-container *ngVar="(bitstreams$ | async) as bitstreams">
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate"> <ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
<div class="file-section"> <div class="file-section">
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?.content" [download]="file?.name"> <a *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
<span>{{file?.name}}</span> <span>{{file?.name}}</span>
<span>({{(file?.sizeBytes) | dsFileSize }})</span> <span>({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span> <span *ngIf="!last" innerHTML="{{separator}}"></span>

View File

@@ -1,8 +1,10 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
/** /**
* This component renders the file section of the item * This component renders the file section of the item
@@ -20,14 +22,21 @@ export class FileSectionComponent implements OnInit {
separator = '<br/>'; separator = '<br/>';
bitstreamsObs: Observable<Bitstream[]>; bitstreams$: Observable<Bitstream[]>;
constructor(
protected bitstreamDataService: BitstreamDataService
) {
}
ngOnInit(): void { ngOnInit(): void {
this.initialize(); this.initialize();
} }
initialize(): void { initialize(): void {
this.bitstreamsObs = this.item.getFiles(); this.bitstreams$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL').pipe(
getFirstSucceededRemoteListPayload()
);
} }
} }

View File

@@ -4,7 +4,7 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>

View File

@@ -1,20 +1,34 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClient } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../../core/shared/item.model'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
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 { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { CommunityDataService } from '../../../../core/data/community-data.service';
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 { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
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 { createRelationshipsObservable } from '../shared/item.component.spec';
import { PublicationComponent } from './publication.component'; import { PublicationComponent } from './publication.component';
import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
const mockItem: Item = Object.assign(new Item(), { const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
@@ -27,6 +41,11 @@ describe('PublicationComponent', () => {
let fixture: ComponentFixture<PublicationComponent>; let fixture: ComponentFixture<PublicationComponent>;
beforeEach(async(() => { beforeEach(async(() => {
const mockBitstreamDataService = {
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
return createSuccessfulRemoteDataObject$(new Bitstream());
}
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -36,14 +55,25 @@ describe('PublicationComponent', () => {
})], })],
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe], declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
providers: [ providers: [
{provide: ItemDataService, useValue: {}}, { provide: ItemDataService, useValue: {} },
{provide: TruncatableService, useValue: {}}, { provide: TruncatableService, useValue: {} },
{provide: RelationshipService, useValue: {}} { provide: RelationshipService, useValue: {} },
{ provide: ObjectCacheService, useValue: {} },
{ provide: UUIDService, useValue: {} },
{ provide: Store, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: CommunityDataService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(PublicationComponent, { }).overrideComponent(PublicationComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -17,4 +17,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PublicationComponent extends ItemComponent { export class PublicationComponent extends ItemComponent {
} }

View File

@@ -1,12 +1,12 @@
import { getAllSucceededRemoteData, getFinishedRemoteData, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { distinctUntilChanged, filter, flatMap, map, switchMap } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import { Item } from '../../../../core/shared/item.model'; 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';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../../core/shared/item.model';
import { getFinishedRemoteData, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
/** /**
* Operator for comparing arrays using a mapping function * Operator for comparing arrays using a mapping function
@@ -18,7 +18,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
export const compareArraysUsing = <T>(mapFn: (t: T) => any) => export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
(a: T[], b: T[]): boolean => { (a: T[], b: T[]): boolean => {
if (!Array.isArray(a) || ! Array.isArray(b)) { if (!Array.isArray(a) || ! Array.isArray(b)) {
return false; return false
} }
const aIds = a.map(mapFn); const aIds = a.map(mapFn);
@@ -72,6 +72,7 @@ export const relationsToItems = (thisId: string) =>
export const paginatedRelationsToItems = (thisId: string) => export const paginatedRelationsToItems = (thisId: string) =>
(source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> => (source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
source.pipe( source.pipe(
getSucceededRemoteData(),
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => { switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
return observableCombineLatest( return observableCombineLatest(
...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem.pipe(getFinishedRemoteData()), rel.rightItem.pipe(getFinishedRemoteData()))) ...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem.pipe(getFinishedRemoteData()), rel.rightItem.pipe(getFinishedRemoteData())))

View File

@@ -1,29 +1,36 @@
import { Item } from '../../../../core/shared/item.model'; import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { Store } from '@ngrx/store';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { isNotEmpty } from '../../../../shared/empty.util';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { ItemComponent } from './item.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models'; import { CommunityDataService } from '../../../../core/data/community-data.service';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils'; import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; 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 { 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 { 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 { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
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';
/** /**
* Create a generic test for an item-page-fields component using a mockItem and the type of component * Create a generic test for an item-page-fields component using a mockItem and the type of component
@@ -38,6 +45,11 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
let fixture: ComponentFixture<any>; let fixture: ComponentFixture<any>;
beforeEach(async(() => { beforeEach(async(() => {
const mockBitstreamDataService = {
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
return createSuccessfulRemoteDataObject$(new Bitstream());
}
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -47,14 +59,25 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
})], })],
declarations: [component, GenericItemPageFieldComponent, TruncatePipe], declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
providers: [ providers: [
{provide: ItemDataService, useValue: {}}, { provide: ItemDataService, useValue: {} },
{provide: TruncatableService, useValue: {}}, { provide: TruncatableService, useValue: {} },
{provide: RelationshipService, useValue: {}} { provide: RelationshipService, useValue: {} },
{ provide: ObjectCacheService, useValue: {} },
{ provide: UUIDService, useValue: {} },
{ provide: Store, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: CommunityDataService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(component, { }).overrideComponent(component, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents(); }).compileComponents();
})); }));
@@ -102,6 +125,7 @@ export function createRelationshipsObservable() {
}) })
])); ]));
} }
describe('ItemComponent', () => { describe('ItemComponent', () => {
const arr1 = [ const arr1 = [
{ {

View File

@@ -1,5 +1,9 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
@Component({ @Component({
selector: 'ds-item', selector: 'ds-item',
@@ -10,4 +14,14 @@ import { Item } from '../../../../core/shared/item.model';
*/ */
export class ItemComponent { export class ItemComponent {
@Input() object: Item; @Input() object: Item;
constructor(protected bitstreamDataService: BitstreamDataService) {
}
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
getThumbnail(): Observable<Bitstream> {
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(
getFirstSucceededRemoteDataPayload()
);
}
} }

View File

@@ -10,6 +10,7 @@ import { Relationship } from '../../../core/shared/item-relationships/relationsh
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component'; import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({ @Component({
@@ -81,7 +82,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) .map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => { .map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) { if (metadatum.isVirtual) {
return this.relationshipService.findById(metadatum.virtualValue).pipe( return this.relationshipService.findById(metadatum.virtualValue, followLink('leftItem'), followLink('rightItem')).pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) => switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(

View File

@@ -1,6 +1,6 @@
import { LookupGuard } from './lookup-guard';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { IdentifierType } from '../core/data/request.models'; import { IdentifierType } from '../core/data/request.models';
import { LookupGuard } from './lookup-guard';
describe('LookupGuard', () => { describe('LookupGuard', () => {
let dsoService: any; let dsoService: any;
@@ -8,13 +8,13 @@ describe('LookupGuard', () => {
beforeEach(() => { beforeEach(() => {
dsoService = { dsoService = {
findById: jasmine.createSpy('findById').and.returnValue(observableOf({ hasFailed: false, findByIdAndIDType: jasmine.createSpy('findByIdAndIDType').and.returnValue(observableOf({ hasFailed: false,
hasSucceeded: true })) hasSucceeded: true }))
}; };
guard = new LookupGuard(dsoService); guard = new LookupGuard(dsoService);
}); });
it('should call findById with handle params', () => { it('should call findByIdAndIDType with handle params', () => {
const scopedRoute = { const scopedRoute = {
params: { params: {
id: '1234', id: '1234',
@@ -22,10 +22,10 @@ describe('LookupGuard', () => {
} }
}; };
guard.canActivate(scopedRoute as any, undefined); guard.canActivate(scopedRoute as any, undefined);
expect(dsoService.findById).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE) expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE)
}); });
it('should call findById with handle params', () => { it('should call findByIdAndIDType with handle params', () => {
const scopedRoute = { const scopedRoute = {
params: { params: {
id: '123456789%2F1234', id: '123456789%2F1234',
@@ -33,10 +33,10 @@ describe('LookupGuard', () => {
} }
}; };
guard.canActivate(scopedRoute as any, undefined); guard.canActivate(scopedRoute as any, undefined);
expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE) expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE)
}); });
it('should call findById with UUID params', () => { it('should call findByIdAndIDType with UUID params', () => {
const scopedRoute = { const scopedRoute = {
params: { params: {
id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
@@ -44,7 +44,7 @@ describe('LookupGuard', () => {
} }
}; };
guard.canActivate(scopedRoute as any, undefined); guard.canActivate(scopedRoute as any, undefined);
expect(dsoService.findById).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID) expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID)
}); });
}); });

View File

@@ -20,7 +20,7 @@ export class LookupGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = this.getLookupParams(route); const params = this.getLookupParams(route);
return this.dsoService.findById(params.id, params.type).pipe( return this.dsoService.findByIdAndIDType(params.id, params.type).pipe(
map((response: RemoteData<FindByIDRequest>) => response.hasFailed) map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
); );
} }

View File

@@ -10,7 +10,7 @@
[options]="config.notifications"> [options]="config.notifications">
</ds-notifications-board> </ds-notifications-board>
<main class="main-content"> <main class="main-content">
<div class="container" *ngIf="isLoading"> <div class="container" *ngIf="isLoading$ | async">
<ds-loading message="{{'loading.default' | translate}}"></ds-loading> <ds-loading message="{{'loading.default' | translate}}"></ds-loading>
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -19,7 +19,7 @@ import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/initial-menus-state'; import { MenuID } from './shared/menu/initial-menus-state';
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
import { slideSidebarPadding } from './shared/animations/slide'; import { slideSidebarPadding } from './shared/animations/slide';
import { HostWindowService } from './shared/host-window.service'; import { HostWindowService } from './shared/host-window.service';
import { Theme } from '../config/theme.inferface'; import { Theme } from '../config/theme.inferface';
@@ -38,7 +38,7 @@ export const LANG_COOKIE = 'language_cookie';
animations: [slideSidebarPadding] animations: [slideSidebarPadding]
}) })
export class AppComponent implements OnInit, AfterViewInit { export class AppComponent implements OnInit, AfterViewInit {
isLoading = true; isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
sidebarVisible: Observable<boolean>; sidebarVisible: Observable<boolean>;
slideSidebarOver: Observable<boolean>; slideSidebarOver: Observable<boolean>;
collapsedSidebarWidth: Observable<string>; collapsedSidebarWidth: Observable<string>;
@@ -131,12 +131,12 @@ export class AppComponent implements OnInit, AfterViewInit {
delay(0) delay(0)
).subscribe((event) => { ).subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
this.isLoading = true; this.isLoading$.next(true);
} else if ( } else if (
event instanceof NavigationEnd || event instanceof NavigationEnd ||
event instanceof NavigationCancel event instanceof NavigationCancel
) { ) {
this.isLoading = false; this.isLoading$.next(false);
} }
}); });
} }

View File

@@ -190,8 +190,6 @@ describe('CommunityListService', () => {
service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store); service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store);
})); }));
afterAll(() => service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store));
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => { it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {
expect(serviceIn).toBeTruthy(); expect(serviceIn).toBeTruthy();
})); }));

View File

@@ -1,6 +1,7 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { EPersonDataService } from '../eperson/eperson-data.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';

View File

@@ -3,15 +3,16 @@ import { async, TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { AuthStatusResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { MockStore } from '../../shared/testing/mock-store'; import { MockStore } from '../../shared/testing/mock-store';
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';
describe('AuthResponseParsingService', () => { describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService; let service: AuthResponseParsingService;
let linkServiceStub: any;
const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any; const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any;
let store: any; let store: any;
@@ -30,7 +31,10 @@ describe('AuthResponseParsingService', () => {
beforeEach(() => { beforeEach(() => {
store = TestBed.get(Store); store = TestBed.get(Store);
objectCacheService = new ObjectCacheService(store as any); linkServiceStub = jasmine.createSpyObj({
removeResolvedLinks: {}
});
objectCacheService = new ObjectCacheService(store as any, linkServiceStub);
service = new AuthResponseParsingService(EnvConfig, objectCacheService); service = new AuthResponseParsingService(EnvConfig, objectCacheService);
}); });
@@ -141,6 +145,7 @@ describe('AuthResponseParsingService', () => {
it('should return a AuthStatusResponse if data contains a valid endpoint response', () => { it('should return a AuthStatusResponse if data contains a valid endpoint response', () => {
const response = service.parse(validRequest2, validResponse2); const response = service.parse(validRequest2, validResponse2);
expect(response.constructor).toBe(AuthStatusResponse); expect(response.constructor).toBe(AuthStatusResponse);
expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalled();
}); });
it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => { it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => {

View File

@@ -10,8 +10,6 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseParsingService } from '../data/parsing.service'; import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models'; import { RestRequest } from '../data/request.models';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
@Injectable() @Injectable()
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
@@ -25,10 +23,10 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) {
const response = this.process<NormalizedObject<AuthStatus>>(data.payload, request); const response = this.process<AuthStatus>(data.payload, request);
return new AuthStatusResponse(response, data.statusCode, data.statusText); return new AuthStatusResponse(response, data.statusCode, data.statusText);
} else { } else {
return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText); return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText);
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { REQUEST } from '@nguniversal/express-engine/tokens'; import { REQUEST } from '@nguniversal/express-engine/tokens';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { LinkService } from '../cache/builders/link.service';
import { authReducer, AuthState } from './auth.reducer'; import { authReducer, AuthState } from './auth.reducer';
import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { NativeWindowRef, NativeWindowService } from '../services/window.service';
@@ -38,7 +39,7 @@ describe('AuthService test', () => {
let storage: CookieService; let storage: CookieService;
let token: AuthTokenInfo; let token: AuthTokenInfo;
let authenticatedState; let authenticatedState;
let rdbService; let linkService;
function init() { function init() {
mockStore = jasmine.createSpyObj('store', { mockStore = jasmine.createSpyObj('store', {
@@ -58,8 +59,10 @@ describe('AuthService test', () => {
}; };
authRequest = new AuthRequestServiceStub(); authRequest = new AuthRequestServiceStub();
routeStub = new ActivatedRouteStub(); routeStub = new ActivatedRouteStub();
rdbService = getMockRemoteDataBuildService(); linkService = {
spyOn(rdbService, 'build').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})}); resolveLinks: {}
};
spyOn(linkService, 'resolveLinks').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})});
} }
@@ -80,7 +83,7 @@ describe('AuthService test', () => {
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },
{ provide: Store, useValue: mockStore }, { provide: Store, useValue: mockStore },
{ provide: RemoteDataBuildService, useValue: rdbService }, { provide: LinkService, useValue: linkService },
CookieService, CookieService,
AuthService AuthService
], ],
@@ -143,7 +146,7 @@ describe('AuthService test', () => {
{ provide: REQUEST, useValue: {} }, { provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: RemoteDataBuildService, useValue: rdbService }, { provide: RemoteDataBuildService, useValue: linkService },
CookieService, CookieService,
AuthService AuthService
] ]
@@ -156,7 +159,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService);
})); }));
it('should return true when user is logged in', () => { it('should return true when user is logged in', () => {
@@ -195,7 +198,7 @@ describe('AuthService test', () => {
{ provide: REQUEST, useValue: {} }, { provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: RemoteDataBuildService, useValue: rdbService }, { provide: RemoteDataBuildService, useValue: linkService },
ClientCookieService, ClientCookieService,
CookieService, CookieService,
AuthService AuthService
@@ -218,7 +221,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService);
storage = (authService as any).storage; storage = (authService as any).storage;
routeServiceMock = TestBed.get(RouteService); routeServiceMock = TestBed.get(RouteService);
routerStub = TestBed.get(Router); routerStub = TestBed.get(Router);

View File

@@ -8,6 +8,8 @@ import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLate
import { RouterReducerState } from '@ngrx/router-store'; import { RouterReducerState } from '@ngrx/router-store';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { LinkService } from '../cache/builders/link.service';
import { EPerson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
@@ -21,8 +23,7 @@ import { AppState, routerStateSelector } from '../../app.reducer';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RouteService } from '../services/route.service';
import {RouteService} from '../services/route.service';
export const LOGIN_ROUTE = '/login'; export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout'; export const LOGOUT_ROUTE = '/logout';
@@ -49,7 +50,7 @@ export class AuthService {
protected routeService: RouteService, protected routeService: RouteService,
protected storage: CookieService, protected storage: CookieService,
protected store: Store<AppState>, protected store: Store<AppState>,
protected rdbService: RemoteDataBuildService protected linkService: LinkService
) { ) {
this.store.pipe( this.store.pipe(
select(isAuthenticated), select(isAuthenticated),
@@ -133,7 +134,7 @@ export class AuthService {
headers = headers.append('Authorization', `Bearer ${token.accessToken}`); headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers; options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe( return this.authRequestService.getRequest('status', options).pipe(
map((status) => this.rdbService.build(status)), map((status) => this.linkService.resolveLinks(status, followLink<AuthStatus>('eperson'))),
switchMap((status: AuthStatus) => { switchMap((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status.eperson.pipe(map((eperson) => eperson.payload)); return status.eperson.pipe(map((eperson) => eperson.payload));

View File

@@ -1,54 +1,83 @@
import { AuthError } from './auth-error.model'; import { autoserialize, deserialize, deserializeAs } from 'cerialize';
import { AuthTokenInfo } from './auth-token-info.model';
import { EPerson } from '../../eperson/models/eperson.model';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { link, typedObject } from '../../cache/builders/build-decorators';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { RemoteData } from '../../data/remote-data';
import { EPerson } from '../../eperson/models/eperson.model';
import { EPERSON } from '../../eperson/models/eperson.resource-type';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { AuthError } from './auth-error.model';
import { AUTH_STATUS } from './auth-status.resource-type';
import { AuthTokenInfo } from './auth-token-info.model';
/** /**
* Object that represents the authenticated status of a user * Object that represents the authenticated status of a user
*/ */
@typedObject
export class AuthStatus implements CacheableObject { export class AuthStatus implements CacheableObject {
static type = new ResourceType('status'); static type = AUTH_STATUS;
/** /**
* The unique identifier of this auth status * The unique identifier of this auth status
*/ */
@autoserialize
id: string; id: string;
/** /**
* The unique uuid of this auth status * The type for this AuthStatus
*/ */
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The UUID of this auth status
* This UUID is generated client-side and isn't used by the backend.
* It is based on the ID, so it will be the same for each refresh.
*/
@deserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
uuid: string; uuid: string;
/** /**
* True if REST API is up and running, should never return false * True if REST API is up and running, should never return false
*/ */
@autoserialize
okay: boolean; okay: boolean;
/** /**
* If the auth status represents an authenticated state * If the auth status represents an authenticated state
*/ */
@autoserialize
authenticated: boolean; authenticated: boolean;
/** /**
* Authentication error if there was one for this status * The {@link HALLink}s for this AuthStatus
*/ */
error?: AuthError; @deserialize
_links: {
self: HALLink;
eperson: HALLink;
};
/** /**
* The eperson of this auth status * The EPerson of this auth status
* Will be undefined unless the eperson {@link HALLink} has been resolved.
*/ */
eperson: Observable<RemoteData<EPerson>>; @link(EPERSON)
eperson?: Observable<RemoteData<EPerson>>;
/** /**
* True if the token is valid, false if there was no token or the token wasn't valid * True if the token is valid, false if there was no token or the token wasn't valid
*/ */
@autoserialize
token?: AuthTokenInfo; token?: AuthTokenInfo;
/** /**
* The self link of this auth status' REST object * Authentication error if there was one for this status
*/ */
self: string; // TODO should be refactored to use the RemoteData error
error?: AuthError;
} }

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for AuthStatus
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const AUTH_STATUS = new ResourceType('status');

View File

@@ -1,41 +0,0 @@
import { AuthStatus } from './auth-status.model';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { NormalizedObject } from '../../cache/models/normalized-object.model';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { EPerson } from '../../eperson/models/eperson.model';
@mapsTo(AuthStatus)
@inheritSerialization(NormalizedObject)
export class NormalizedAuthStatus extends NormalizedObject<AuthStatus> {
/**
* The unique identifier of this auth status
*/
@autoserialize
id: string;
/**
* The unique generated uuid of this auth status
*/
@autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
uuid: string;
/**
* True if REST API is up and running, should never return false
*/
@autoserialize
okay: boolean;
/**
* True if the token is valid, false if there was no token or the token wasn't valid
*/
@autoserialize
authenticated: boolean;
/**
* The self link to the eperson of this auth status
*/
@relationship(EPerson, false)
@autoserialize
eperson: string;
}

View File

@@ -1,15 +1,16 @@
import { filter, map, switchMap, take } from 'rxjs/operators'; import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpHeaders } from '@angular/common/http'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { isNotEmpty } from '../../shared/empty.util';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { AuthService, LOGIN_ROUTE } from './auth.service';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { CheckAuthenticationTokenAction } from './auth.actions';
import { EPerson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { CheckAuthenticationTokenAction } from './auth.actions';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
/** /**
* The auth service. * The auth service.
@@ -34,7 +35,7 @@ export class ServerAuthService extends AuthService {
options.headers = headers; options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe( return this.authRequestService.getRequest('status', options).pipe(
map((status) => this.rdbService.build(status)), map((status) => this.linkService.resolveLinks(status, followLink<AuthStatus>('eperson'))),
switchMap((status: AuthStatus) => { switchMap((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status.eperson.pipe(map((eperson) => eperson.payload)); return status.eperson.pipe(map((eperson) => eperson.payload));

View File

@@ -1,16 +1,16 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { RequestEntry } from '../data/request.reducer'; import { BrowseService } from './browse.service';
import { of as observableOf } from 'rxjs';
describe('BrowseService', () => { describe('BrowseService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -44,8 +44,8 @@ describe('BrowseService', () => {
'dc.date.issued' 'dc.date.issued'
], ],
_links: { _links: {
self: 'https://rest.api/discover/browses/dateissued', self: { href: 'https://rest.api/discover/browses/dateissued' },
items: 'https://rest.api/discover/browses/dateissued/items' items: { href: 'https://rest.api/discover/browses/dateissued/items' }
} }
}), }),
Object.assign(new BrowseDefinition(), { Object.assign(new BrowseDefinition(), {
@@ -72,9 +72,9 @@ describe('BrowseService', () => {
'dc.creator' 'dc.creator'
], ],
_links: { _links: {
self: 'https://rest.api/discover/browses/author', self: { href: 'https://rest.api/discover/browses/author' },
entries: 'https://rest.api/discover/browses/author/entries', entries: { href: 'https://rest.api/discover/browses/author/entries' },
items: 'https://rest.api/discover/browses/author/items' items: { href: 'https://rest.api/discover/browses/author/items' }
} }
}) })
]; ];
@@ -125,9 +125,11 @@ describe('BrowseService', () => {
}); });
it('should return a RemoteData object containing the correct BrowseDefinition[]', () => { it('should return a RemoteData object containing the correct BrowseDefinition[]', () => {
const expected = cold('--a-', { a: { const expected = cold('--a-', {
payload: browseDefinitions a: {
}}); payload: browseDefinitions
}
});
expect(service.getBrowseDefinitions()).toBeObservable(expected); expect(service.getBrowseDefinitions()).toBeObservable(expected);
}); });
@@ -142,15 +144,17 @@ describe('BrowseService', () => {
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
spyOn(service, 'getBrowseDefinitions').and spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', { a: { .returnValue(hot('--a-', {
a: {
payload: browseDefinitions payload: browseDefinitions
}})); }
}));
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
}); });
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should configure a new BrowseEntriesRequest', () => { it('should configure a new BrowseEntriesRequest', () => {
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries); const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href);
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
@@ -169,7 +173,7 @@ describe('BrowseService', () => {
describe('when getBrowseItemsFor is called with a valid browse definition id', () => { describe('when getBrowseItemsFor is called with a valid browse definition id', () => {
it('should configure a new BrowseItemsRequest', () => { it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName); const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
@@ -215,9 +219,11 @@ describe('BrowseService', () => {
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
spyOn(service, 'getBrowseDefinitions').and spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', { a: { .returnValue(hot('--a-', {
a: {
payload: browseDefinitions payload: browseDefinitions
}})); }
}));
}); });
it('should return the URL for the given metadataKey and linkPath', () => { it('should return the URL for the given metadataKey and linkPath', () => {
@@ -288,14 +294,16 @@ describe('BrowseService', () => {
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
spyOn(service, 'getBrowseDefinitions').and spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('--a-', { a: { .returnValue(hot('--a-', {
a: {
payload: browseDefinitions payload: browseDefinitions
}})); }
}));
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
}); });
describe('when getFirstItemFor is called with a valid browse definition id', () => { describe('when getFirstItemFor is called with a valid browse definition id', () => {
const expectedURL = browseDefinitions[1]._links.items + '?page=0&size=1'; const expectedURL = browseDefinitions[1]._links.items.href + '?page=0&size=1';
it('should configure a new BrowseItemsRequest', () => { it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), expectedURL); const expected = new BrowseItemsRequest(requestService.generateRequestId(), expectedURL);

View File

@@ -10,18 +10,16 @@ import {
isNotEmptyOperator isNotEmptyOperator
} from '../../shared/empty.util'; } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; 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';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest, RestRequest } from '../data/request.models';
BrowseEndpointRequest,
BrowseEntriesRequest,
BrowseItemsRequest,
RestRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model'; import { BrowseEntry } from '../shared/browse-entry.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
import { import {
configureRequest, configureRequest,
filterSuccessfulResponses, filterSuccessfulResponses,
@@ -31,10 +29,7 @@ import {
getRequestFromRequestHref getRequestFromRequestHref
} from '../shared/operators'; } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { GenericSuccessResponse } from '../cache/response.models';
/** /**
* The service handling all browse requests * The service handling all browse requests
@@ -81,10 +76,11 @@ export class BrowseService {
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload), map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(), ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions map((definitions: BrowseDefinition[]) => definitions
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))), .map((definition: BrowseDefinition) => {
distinctUntilChanged() return Object.assign(new BrowseDefinition(), definition)
})),
distinctUntilChanged(),
); );
return this.rdb.toRemoteDataObservable(requestEntry$, payload$); return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
} }
@@ -96,7 +92,10 @@ export class BrowseService {
return this.getBrowseDefinitions().pipe( return this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(options.metadataDefinition), getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => _links.entries), map((_links: any) => {
const entriesLink = _links.entries.href || _links.entries;
return entriesLink;
}),
hasValueOperator(), hasValueOperator(),
map((href: string) => { map((href: string) => {
// TODO nearly identical to PaginatedSearchOptions => refactor // TODO nearly identical to PaginatedSearchOptions => refactor
@@ -133,7 +132,10 @@ export class BrowseService {
return this.getBrowseDefinitions().pipe( return this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(options.metadataDefinition), getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => _links.items), map((_links: any) => {
const itemsLink = _links.items.href || _links.items;
return itemsLink;
}),
hasValueOperator(), hasValueOperator(),
map((href: string) => { map((href: string) => {
const args = []; const args = [];
@@ -171,7 +173,10 @@ export class BrowseService {
return this.getBrowseDefinitions().pipe( return this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definition), getBrowseDefinitionLinks(definition),
hasValueOperator(), hasValueOperator(),
map((_links: any) => _links.items), map((_links: any) => {
const itemsLink = _links.items.href || _links.items;
return itemsLink;
}),
hasValueOperator(), hasValueOperator(),
map((href: string) => { map((href: string) => {
const args = []; const args = [];
@@ -249,7 +254,7 @@ export class BrowseService {
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`); throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`);
} else { } else {
return def._links[linkPath]; return def._links[linkPath] || def._links[linkPath].href;
} }
}), }),
startWith(undefined), startWith(undefined),

View File

@@ -0,0 +1,83 @@
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import {
dataService,
getDataServiceFor,
getLinkDefinition,
link,
} from './build-decorators';
/* tslint:disable:max-classes-per-file */
class TestService {}
class AnotherTestService {}
class TestHALResource implements HALResource {
_links: {
self: HALLink;
foo: HALLink;
};
bar?: any
}
let testType;
describe('build decorators', () => {
beforeEach(() => {
testType = new ResourceType('testType-' + new Date().getTime());
});
describe('@dataService/getDataServiceFor', () => {
it('should register a resourcetype for a dataservice', () => {
dataService(testType)(TestService);
expect(getDataServiceFor(testType)).toBe(TestService);
});
describe(`when the resource type isn't specified`, () => {
it(`should throw an error`, () => {
expect(() => {
dataService(undefined)(TestService);
}).toThrow();
});
});
describe(`when there already is a registered dataservice for a resourcetype`, () => {
it(`should throw an error`, () => {
dataService(testType)(TestService);
expect(() => {
dataService(testType)(AnotherTestService);
}).toThrow();
});
});
});
describe(`@link/getLinkDefinitions`, () => {
it(`should register a link`, () => {
const target = new TestHALResource();
link(testType, true, 'foo')(target, 'bar');
const result = getLinkDefinition(TestHALResource, 'foo');
expect(result.resourceType).toBe(testType);
expect(result.isList).toBe(true);
expect(result.linkName).toBe('foo');
expect(result.propertyName).toBe('bar');
});
describe(`when the linkname isn't specified`, () => {
it(`should use the propertyname`, () => {
const target = new TestHALResource();
link(testType)(target, 'foo');
const result = getLinkDefinition(TestHALResource, 'foo');
expect(result.linkName).toBe('foo');
expect(result.propertyName).toBe('foo');
});
});
describe(`when there's no @link`, () => {
it(`should return undefined`, () => {
const result = getLinkDefinition(TestHALResource, 'self');
expect(result).toBeUndefined();
});
});
});
});
/* tslint:enable:max-classes-per-file */

View File

@@ -1,80 +1,161 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { DataService } from '../../data/data.service';
import { GenericConstructor } from '../../shared/generic-constructor'; import { GenericConstructor } from '../../shared/generic-constructor';
import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { CacheableObject, TypedObject } from '../object-cache.reducer';
const mapsToMetadataKey = Symbol('mapsTo'); const resolvedLinkKey = Symbol('resolvedLink');
const relationshipKey = Symbol('relationship');
const relationshipMap = new Map(); const resolvedLinkMap = new Map();
const typeMap = new Map(); const typeMap = new Map();
const dataServiceMap = new Map();
const linkMap = new Map();
/** /**
* Decorator function to map a normalized class to it's not-normalized counter part class * Decorator function to map a ResourceType to its class
* It will also maps a type to the matching class * @param target The contructor of the typed class to map
* @param value The not-normalized class to map to
*/ */
export function mapsTo(value: GenericConstructor<TypedObject>) { export function typedObject(target: typeof TypedObject) {
return function decorator(objectConstructor: GenericConstructor<TypedObject>) { typeMap.set(target.type.value, target);
Reflect.defineMetadata(mapsToMetadataKey, value, objectConstructor);
mapsToType((value as any).type, objectConstructor);
}
}
/**
* Maps a type to the matching class
* @param value The resourse type
* @param objectConstructor The class to map to
*/
function mapsToType(value: ResourceType, objectConstructor: GenericConstructor<TypedObject>) {
if (!objectConstructor || !value) {
return;
}
typeMap.set(value.value, objectConstructor);
}
/**
* Returns the mapped class for the given normalized class
* @param target The normalized class
*/
export function getMapsTo(target: any) {
return Reflect.getOwnMetadata(mapsToMetadataKey, target);
} }
/** /**
* Returns the mapped class for the given type * Returns the mapped class for the given type
* @param type The resource type * @param type The resource type
*/ */
export function getMapsToType(type: string | ResourceType) { export function getClassForType(type: string | ResourceType) {
if (typeof(type) === 'object') { if (typeof(type) === 'object') {
type = (type as ResourceType).value; type = (type as ResourceType).value;
} }
return typeMap.get(type); return typeMap.get(type);
} }
export function relationship<T extends CacheableObject>(value: GenericConstructor<T>, isList: boolean = false): any { /**
return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) { * A class decorator to indicate that this class is a dataservice
if (!target || !propertyKey) { * for a given resource type.
return; *
* "dataservice" in this context means that it has findByHref and
* findAllByHref methods.
*
* @param resourceType the resource type the class is a dataservice for
*/
export function dataService(resourceType: ResourceType): any {
return (target: any) => {
if (hasNoValue(resourceType)) {
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
}
const existingDataservice = dataServiceMap.get(resourceType.value);
if (hasValue(existingDataservice)) {
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
} }
const metaDataList: string[] = relationshipMap.get(target.constructor) || []; dataServiceMap.set(resourceType.value, target);
if (metaDataList.indexOf(propertyKey) === -1) {
metaDataList.push(propertyKey);
}
relationshipMap.set(target.constructor, metaDataList);
return Reflect.metadata(relationshipKey, {
resourceType: (value as any).type.value,
isList
}).apply(this, arguments);
}; };
} }
export function getRelationMetadata(target: any, propertyKey: string) { /**
return Reflect.getMetadata(relationshipKey, target, propertyKey); * Return the dataservice matching the given resource type
*
* @param resourceType the resource type you want the matching dataservice for
*/
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
return dataServiceMap.get(resourceType.value);
} }
export function getRelationships(target: any) { /**
return relationshipMap.get(target); * A class to represent the data that can be set by the @link decorator
*/
export class LinkDefinition<T extends HALResource> {
resourceType: ResourceType;
isList = false;
linkName: keyof T['_links'];
propertyName: keyof T;
}
/**
* A property decorator to indicate that a certain property is the placeholder
* where the contents of a resolved link should be stored.
*
* e.g. if an Item has an hal link for bundles, and an item.bundles property
* this decorator should decorate that item.bundles property.
*
* @param resourceType the resource type of the object(s) the link retrieves
* @param isList an optional boolean indicating whether or not it concerns a list,
* defaults to false
* @param linkName an optional string in case the {@link HALLink} name differs from the
* property name
*/
export const link = <T extends HALResource>(
resourceType: ResourceType,
isList = false,
linkName?: keyof T['_links'],
) => {
return (target: T, propertyName: string) => {
let targetMap = linkMap.get(target.constructor);
if (hasNoValue(targetMap)) {
targetMap = new Map<keyof T['_links'],LinkDefinition<T>>();
}
if (hasNoValue(linkName)) {
linkName = propertyName as any;
}
targetMap.set(linkName, {
resourceType,
isList,
linkName,
propertyName
});
linkMap.set(target.constructor, targetMap);
}
};
/**
* Returns all LinkDefinitions for a model class
* @param source
*/
export const getLinkDefinitions = <T extends HALResource>(source: GenericConstructor<T>): Map<keyof T['_links'], LinkDefinition<T>> => {
return linkMap.get(source);
};
/**
* Returns a specific LinkDefinition for a model class
*
* @param source the model class
* @param linkName the name of the link
*/
export const getLinkDefinition = <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']): LinkDefinition<T> => {
const sourceMap = linkMap.get(source);
if (hasValue(sourceMap)) {
return sourceMap.get(linkName);
} else {
return undefined;
}
};
/**
* A class level decorator to indicate you want to inherit @link annotations
* from a parent class.
*
* @param parent the parent class to inherit @link annotations from
*/
export function inheritLinkAnnotations(parent: any): any {
return (child: any) => {
const parentMap: Map<string, LinkDefinition<any>> = linkMap.get(parent) || new Map();
const childMap: Map<string, LinkDefinition<any>> = linkMap.get(child) || new Map();
parentMap.forEach((value, key) => {
if (!childMap.has(key)) {
childMap.set(key, value);
}
});
linkMap.set(child, childMap);
}
} }

View File

@@ -0,0 +1,222 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../../data/request.models';
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import * as decorators from './build-decorators';
import { getDataServiceFor } from './build-decorators';
import { LinkService } from './link.service';
const spyOnFunction = <T>(obj: T, func: keyof T) => {
const spy = jasmine.createSpy(func as string);
spyOnProperty(obj, func, 'get').and.returnValue(spy);
return spy;
};
const TEST_MODEL = new ResourceType('testmodel');
let result: any;
/* tslint:disable:max-classes-per-file */
class TestModel implements HALResource {
static type = TEST_MODEL;
type = TEST_MODEL;
value: string;
_links: {
self: HALLink;
predecessor: HALLink;
successor: HALLink;
};
predecessor?: TestModel;
successor?: TestModel;
}
@Injectable()
class TestDataService {
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<any>>) {
return 'findAllByHref'
}
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<any>>) {
return 'findByHref'
}
}
let testDataService: TestDataService;
let testModel: TestModel;
describe('LinkService', () => {
let service: LinkService;
beforeEach(() => {
testModel = Object.assign(new TestModel(), {
value: 'a test value',
_links: {
self: {
href: 'http://self.link'
},
predecessor: {
href: 'http://predecessor.link'
},
successor: {
href: 'http://successor.link'
},
}
});
testDataService = new TestDataService();
spyOn(testDataService, 'findAllByHref').and.callThrough();
spyOn(testDataService, 'findByHref').and.callThrough();
TestBed.configureTestingModule({
providers: [LinkService, {
provide: TestDataService,
useValue: testDataService
}]
});
service = TestBed.get(LinkService);
});
describe('resolveLink', () => {
describe(`when the linkdefinition concerns a single object`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
});
it('should call dataservice.findByHref with the correct href and nested links', () => {
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, followLink('successor'));
});
});
describe(`when the linkdefinition concerns a list`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
isList: true
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', { some: 'options '} as any, 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'));
});
});
describe('either way', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
});
it('should call getLinkDefinition with the correct model and link', () => {
expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
});
it('should call getDataServiceFor with the correct resource type', () => {
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
});
it('should return the model with the resolved link', () => {
expect(result.type).toBe(TEST_MODEL);
expect(result.value).toBe('a test value');
expect(result._links.self.href).toBe('http://self.link');
expect(result.predecessor).toBe('findByHref');
});
});
describe(`when the specified link doesn't exist on the model's class`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
}).toThrow();
});
});
describe(`when there is no dataservice for the resourcetype in the link`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
}).toThrow();
});
});
});
describe('resolveLinks', () => {
beforeEach(() => {
spyOn(service, 'resolveLink');
service.resolveLinks(testModel, followLink('predecessor'), followLink('successor'))
});
it('should call resolveLink with the model for each of the provided links', () => {
expect(service.resolveLink).toHaveBeenCalledWith(testModel, followLink('predecessor'));
expect(service.resolveLink).toHaveBeenCalledWith(testModel, followLink('successor'));
});
it('should return the model', () => {
expect(result.type).toBe(TEST_MODEL);
expect(result.value).toBe('a test value');
expect(result._links.self.href).toBe('http://self.link');
});
});
describe('removeResolvedLinks', () => {
beforeEach(() => {
testModel.predecessor = 'predecessor value' as any;
testModel.successor = 'successor value' as any;
spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([
{
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
},
{
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor',
}
])
});
it('should return a new version of the object without any resolved links', () => {
result = service.removeResolvedLinks(testModel);
expect(result.value).toBe(testModel.value);
expect(result.type).toBe(testModel.type);
expect(result._links).toBe(testModel._links);
expect(result.predecessor).toBeUndefined();
expect(result.successor).toBeUndefined();
});
it('should leave the original object untouched', () => {
service.removeResolvedLinks(testModel);
expect(testModel.predecessor as any).toBe('predecessor value');
expect(testModel.successor as any).toBe('successor value');
});
});
});
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,90 @@
import { Injectable, Injector } from '@angular/core';
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators';
/**
* A Service to handle the resolving and removing
* of resolved {@link HALLink}s on HALResources
*/
@Injectable({
providedIn: 'root'
})
export class LinkService {
constructor(
protected parentInjector: Injector,
) {
}
/**
* Resolve the given {@link FollowLinkConfig}s for the given model
*
* @param model the {@link HALResource} to resolve the links for
* @param linksToFollow the {@link FollowLinkConfig}s to resolve
*/
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: Array<FollowLinkConfig<T>>): T {
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
this.resolveLink(model, linkToFollow);
});
return model;
}
/**
* Resolve the given {@link FollowLinkConfig} for the given model
*
* @param model the {@link HALResource} to resolve the link for
* @param linkToFollow the {@link FollowLinkConfig} to resolve
*/
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
if (hasNoValue(matchingLinkDef)) {
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
} else {
const provider = getDataServiceFor(matchingLinkDef.resourceType);
if (hasNoValue(provider)) {
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
}
const service = Injector.create({
providers: [],
parent: this.parentInjector
}).get(provider);
const href = model._links[matchingLinkDef.linkName].href;
try {
if (matchingLinkDef.isList) {
model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow);
} else {
model[linkToFollow.name] = service.findByHref(href, ...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}`);
}
}
return model;
}
/**
* Remove any resolved links that the model may have.
*
* @param model the {@link HALResource} to remove the links from
* @returns a copy of the given model, without resolved links.
*/
public removeResolvedLinks<T extends HALResource>(model: T): T {
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor<T>);
if (isNotEmpty(linkDefs)) {
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
result[linkDef.propertyName] = undefined;
});
}
return result;
}
}

View File

@@ -1,48 +0,0 @@
import { Injectable } from '@angular/core';
import { NormalizedObject } from '../models/normalized-object.model';
import { getMapsToType, getRelationships } from './build-decorators';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { CacheableObject, TypedObject } from '../object-cache.reducer';
/**
* Return true if halObj has a value for `_links.self`
*
* @param {any} halObj The object to test
*/
export function isRestDataObject(halObj: any): boolean {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
}
/**
* Return true if halObj has a value for `page` and `_embedded`
*
* @param {any} halObj The object to test
*/
export function isRestPaginatedList(halObj: any): boolean {
return hasValue(halObj.page) && hasValue(halObj._embedded);
}
/**
* A service to turn domain models in to their normalized
* counterparts.
*/
@Injectable()
export class NormalizedObjectBuildService {
/**
* Returns the normalized model that corresponds to the given domain model
*
* @param {TDomain} domainModel a domain model
*/
normalize<T extends CacheableObject>(domainModel: T): NormalizedObject<T> {
const normalizedConstructor = getMapsToType((domainModel as any).type);
const relationships = getRelationships(normalizedConstructor) || [];
const normalizedModel = Object.assign({}, domainModel) as any;
relationships.forEach((key: string) => {
if (hasValue(normalizedModel[key])) {
normalizedModel[key] = normalizedModel._links[key];
}
});
return normalizedModel;
}
}

View File

@@ -1,10 +1,10 @@
import { RemoteDataBuildService } from './remote-data-build.service';
import { Item } from '../../shared/item.model';
import { PaginatedList } from '../../data/paginated-list';
import { PageInfo } from '../../shared/page-info.model';
import { RemoteData } from '../../data/remote-data';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
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 pageInfo = new PageInfo();
const array = [ const array = [
@@ -37,7 +37,7 @@ describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService; let service: RemoteDataBuildService;
beforeEach(() => { beforeEach(() => {
service = new RemoteDataBuildService(undefined, undefined); service = new RemoteDataBuildService(undefined, undefined, undefined);
}); });
describe('when toPaginatedList is called', () => { describe('when toPaginatedList is called', () => {

View File

@@ -1,37 +1,46 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
import {
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; hasValue,
hasValueOperator,
isEmpty,
isNotEmpty,
isNotUndefined
} from '../../../shared/empty.util';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error'; import { RemoteDataError } from '../../data/remote-data-error';
import { GetRequest } from '../../data/request.models';
import { RequestEntry } from '../../data/request.reducer'; import { RequestEntry } from '../../data/request.reducer';
import { RequestService } from '../../data/request.service'; import { RequestService } from '../../data/request.service';
import { NormalizedObject } from '../models/normalized-object.model';
import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { PageInfo } from '../../shared/page-info.model';
import { import {
filterSuccessfulResponses, filterSuccessfulResponses,
getRequestFromRequestHref, getRequestFromRequestHref,
getRequestFromRequestUUID, getRequestFromRequestUUID,
getResourceLinksFromResponse getResourceLinksFromResponse
} from '../../shared/operators'; } from '../../shared/operators';
import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { PageInfo } from '../../shared/page-info.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { CacheableObject } from '../object-cache.reducer';
import { deepClone } from 'fast-json-patch'; import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
import { LinkService } from './link.service';
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
constructor(protected objectCache: ObjectCacheService, constructor(protected objectCache: ObjectCacheService,
protected linkService: LinkService,
protected requestService: RequestService) { protected requestService: RequestService) {
} }
buildSingle<T extends CacheableObject>(href$: string | Observable<string>): Observable<RemoteData<T>> { /**
* 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<T extends CacheableObject>(href$: string | Observable<string>, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
if (typeof href$ === 'string') { if (typeof href$ === 'string') {
href$ = observableOf(href$); href$ = observableOf(href$);
} }
@@ -71,9 +80,9 @@ export class RemoteDataBuildService {
} }
}), }),
hasValueOperator(), hasValueOperator(),
map((normalized: NormalizedObject<T>) => { map((obj: T) =>
return this.build<T>(normalized); this.linkService.resolveLinks(obj, ...linksToFollow)
}), ),
startWith(undefined), startWith(undefined),
distinctUntilChanged() distinctUntilChanged()
); );
@@ -109,7 +118,13 @@ export class RemoteDataBuildService {
); );
} }
buildList<T extends CacheableObject>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<T>>> { /**
* Creates a list of {@link RemoteData} objects 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 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<T extends CacheableObject>(href$: string | Observable<string>, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
if (typeof href$ === 'string') { if (typeof href$ === 'string') {
href$ = observableOf(href$); href$ = observableOf(href$);
} }
@@ -119,10 +134,10 @@ export class RemoteDataBuildService {
getResourceLinksFromResponse(), getResourceLinksFromResponse(),
switchMap((resourceUUIDs: string[]) => { switchMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs).pipe( return this.objectCache.getList(resourceUUIDs).pipe(
map((normList: Array<NormalizedObject<T>>) => { map((objs: T[]) => {
return normList.map((normalized: NormalizedObject<T>) => { return objs.map((obj: T) =>
return this.build<T>(normalized); this.linkService.resolveLinks(obj, ...linksToFollow)
}); );
})); }));
}), }),
distinctUntilChanged(), distinctUntilChanged(),
@@ -150,54 +165,6 @@ export class RemoteDataBuildService {
return this.toRemoteDataObservable(requestEntry$, payload$); return this.toRemoteDataObservable(requestEntry$, payload$);
} }
build<T extends CacheableObject>(normalized: NormalizedObject<T>): T {
const links: any = {};
const relationships = getRelationships(normalized.constructor) || [];
relationships.forEach((relationship: string) => {
let result;
if (hasValue(normalized[relationship])) {
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
const objectList = normalized[relationship].page || normalized[relationship];
if (typeof objectList !== 'string') {
objectList.forEach((href: string) => {
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href))
});
const rdArr = [];
objectList.forEach((href: string) => {
rdArr.push(this.buildSingle(href));
});
if (isList) {
result = this.aggregate(rdArr);
} else if (rdArr.length === 1) {
result = rdArr[0];
}
} else {
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), objectList));
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
// but it should still be built as a list
if (isList) {
result = this.buildList(objectList);
} else {
result = this.buildSingle(objectList);
}
}
if (hasValue(normalized[relationship].page)) {
links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo);
} else {
links[relationship] = result;
}
}
});
const domainModel = getMapsTo(normalized.constructor);
return Object.assign(new domainModel(), normalized, links);
}
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> { aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
if (isEmpty(input)) { if (isEmpty(input)) {

View File

@@ -1,30 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ItemType } from '../../../shared/item-relationships/item-type.model';
import { mapsTo } from '../../builders/build-decorators';
import { NormalizedObject } from '../normalized-object.model';
import { IDToUUIDSerializer } from '../../id-to-uuid-serializer';
/**
* Normalized model class for a DSpace ItemType
*/
@mapsTo(ItemType)
@inheritSerialization(NormalizedObject)
export class NormalizedItemType extends NormalizedObject<ItemType> {
/**
* The label that describes the ResourceType of the Item
*/
@autoserialize
label: string;
/**
* The identifier of this ItemType
*/
@autoserialize
id: string;
/**
* The universally unique identifier of this ItemType
*/
@autoserializeAs(new IDToUUIDSerializer(ItemType.type.value), 'id')
uuid: string;
}

View File

@@ -1,77 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { RelationshipType } from '../../../shared/item-relationships/relationship-type.model';
import { ResourceType } from '../../../shared/resource-type';
import { mapsTo, relationship } from '../../builders/build-decorators';
import { NormalizedDSpaceObject } from '../normalized-dspace-object.model';
import { NormalizedObject } from '../normalized-object.model';
import { IDToUUIDSerializer } from '../../id-to-uuid-serializer';
import { ItemType } from '../../../shared/item-relationships/item-type.model';
/**
* Normalized model class for a DSpace RelationshipType
*/
@mapsTo(RelationshipType)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedRelationshipType extends NormalizedObject<RelationshipType> {
/**
* The identifier of this RelationshipType
*/
@autoserialize
id: string;
/**
* The label that describes the Relation to the left of this RelationshipType
*/
@autoserialize
leftwardType: string;
/**
* The maximum amount of Relationships allowed to the left of this RelationshipType
*/
@autoserialize
leftMaxCardinality: number;
/**
* The minimum amount of Relationships allowed to the left of this RelationshipType
*/
@autoserialize
leftMinCardinality: number;
/**
* The label that describes the Relation to the right of this RelationshipType
*/
@autoserialize
rightwardType: string;
/**
* The maximum amount of Relationships allowed to the right of this RelationshipType
*/
@autoserialize
rightMaxCardinality: number;
/**
* The minimum amount of Relationships allowed to the right of this RelationshipType
*/
@autoserialize
rightMinCardinality: number;
/**
* The type of Item found to the left of this RelationshipType
*/
@autoserialize
@relationship(ItemType, false)
leftType: string;
/**
* The type of Item found to the right of this RelationshipType
*/
@autoserialize
@relationship(ItemType, false)
rightType: string;
/**
* The universally unique identifier of this RelationshipType
*/
@autoserializeAs(new IDToUUIDSerializer(RelationshipType.type.value), 'id')
uuid: string;
}

View File

@@ -1,72 +0,0 @@
import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
import { Relationship } from '../../../shared/item-relationships/relationship.model';
import { mapsTo, relationship } from '../../builders/build-decorators';
import { NormalizedObject } from '../normalized-object.model';
import { IDToUUIDSerializer } from '../../id-to-uuid-serializer';
import { RelationshipType } from '../../../shared/item-relationships/relationship-type.model';
import { Item } from '../../../shared/item.model';
/**
* Normalized model class for a DSpace Relationship
*/
@mapsTo(Relationship)
@inheritSerialization(NormalizedObject)
export class NormalizedRelationship extends NormalizedObject<Relationship> {
/**
* The identifier of this Relationship
*/
@deserialize
id: string;
/**
* The item to the left of this relationship
*/
@deserialize
@relationship(Item, false)
leftItem: string;
/**
* The item to the right of this relationship
*/
@deserialize
@relationship(Item, false)
rightItem: string;
/**
* The place of the Item to the left side of this Relationship
*/
@autoserialize
leftPlace: number;
/**
* The place of the Item to the right side of this Relationship
*/
@autoserialize
rightPlace: number;
/**
* The name variant of the Item to the left side of this Relationship
*/
@autoserialize
leftwardValue: string;
/**
* The name variant of the Item to the right side of this Relationship
*/
@autoserialize
rightwardValue: string;
/**
* The type of Relationship
*/
@deserialize
@relationship(RelationshipType, false)
relationshipType: string;
/**
* The universally unique identifier of this Relationship
*/
@deserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id')
uuid: string;
}

View File

@@ -1,65 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { BitstreamFormat } from '../../shared/bitstream-format.model';
import { mapsTo } from '../builders/build-decorators';
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
import { NormalizedObject } from './normalized-object.model';
import { BitstreamFormatSupportLevel } from '../../shared/bitstream-format-support-level';
/**
* Normalized model class for a Bitstream Format
*/
@mapsTo(BitstreamFormat)
@inheritSerialization(NormalizedObject)
export class NormalizedBitstreamFormat extends NormalizedObject<BitstreamFormat> {
/**
* Short description of this Bitstream Format
*/
@autoserialize
shortDescription: string;
/**
* Description of this Bitstream Format
*/
@autoserialize
description: string;
/**
* String representing the MIME type of this Bitstream Format
*/
@autoserialize
mimetype: string;
/**
* The level of support the system offers for this Bitstream Format
*/
@autoserialize
supportLevel: BitstreamFormatSupportLevel;
/**
* True if the Bitstream Format is used to store system information, rather than the content of items in the system
*/
@autoserialize
internal: boolean;
/**
* String representing this Bitstream Format's file extension
*/
@autoserialize
extensions: string[];
/**
* Identifier for this Bitstream Format
* Note that this ID is unique for bitstream formats,
* but might not be unique across different object types
*/
@autoserialize
id: string;
/**
* Universally unique identifier for this Bitstream Format
* Consist of a prefix and the id field to ensure the identifier is unique across all object types
*/
@autoserializeAs(new IDToUUIDSerializer('bitstream-format'), 'id')
uuid: string;
}

View File

@@ -1,60 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Bitstream } from '../../shared/bitstream.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { Item } from '../../shared/item.model';
import { BitstreamFormat } from '../../shared/bitstream-format.model';
/**
* Normalized model class for a DSpace Bitstream
*/
@mapsTo(Bitstream)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedBitstream extends NormalizedDSpaceObject<Bitstream> {
/**
* The size of this bitstream in bytes
*/
@autoserialize
sizeBytes: number;
/**
* The relative path to this Bitstream's file
*/
@autoserialize
content: string;
/**
* The format of this Bitstream
*/
@autoserialize
@relationship(BitstreamFormat, false)
format: string;
/**
* The description of this Bitstream
*/
@autoserialize
description: string;
/**
* An array of Bundles that are direct parents of this Bitstream
*/
@autoserialize
@relationship(Item, true)
parents: string[];
/**
* The Bundle that owns this Bitstream
*/
@autoserialize
@relationship(Item, false)
owner: string;
/**
* The name of the Bundle this Bitstream is part of
*/
@autoserialize
bundleName: string;
}

View File

@@ -1,45 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Bundle } from '../../shared/bundle.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { Bitstream } from '../../shared/bitstream.model';
/**
* Normalized model class for a DSpace Bundle
*/
@mapsTo(Bundle)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedBundle extends NormalizedDSpaceObject<Bundle> {
/**
* The bundle's name
*/
@autoserialize
name: string;
/**
* The primary bitstream of this Bundle
*/
@autoserialize
@relationship(Bitstream, false)
primaryBitstream: string;
/**
* An array of Items that are direct parents of this Bundle
*/
parents: string[];
/**
* The Item that owns this Bundle
*/
owner: string;
/**
* List of Bitstreams that are part of this Bundle
*/
@autoserialize
@relationship(Bitstream, true)
bitstreams: string[];
}

View File

@@ -1,71 +0,0 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Collection } from '../../shared/collection.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
import { NormalizedBitstream } from './normalized-bitstream.model';
import { NormalizedCommunity } from './normalized-community.model';
import { NormalizedItem } from './normalized-item.model';
import { License } from '../../shared/license.model';
import { ResourcePolicy } from '../../shared/resource-policy.model';
import { Bitstream } from '../../shared/bitstream.model';
import { Community } from '../../shared/community.model';
import { Item } from '../../shared/item.model';
/**
* Normalized model class for a DSpace Collection
*/
@mapsTo(Collection)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedCollection extends NormalizedDSpaceObject<Collection> {
/**
* A string representing the unique handle of this Collection
*/
@autoserialize
handle: string;
/**
* The Bitstream that represents the license of this Collection
*/
@autoserialize
@relationship(License, false)
license: string;
/**
* The Bitstream that represents the default Access Conditions of this Collection
*/
@autoserialize
@relationship(ResourcePolicy, false)
defaultAccessConditions: string;
/**
* The Bitstream that represents the logo of this Collection
*/
@deserialize
@relationship(Bitstream, false)
logo: string;
/**
* An array of Communities that are direct parents of this Collection
*/
@deserialize
@relationship(Community, true)
parents: string[];
/**
* The Community that owns this Collection
*/
@deserialize
@relationship(Community, false)
owner: string;
/**
* List of Items that are part of (not necessarily owned by) this Collection
*/
@deserialize
@relationship(Item, true)
items: string[];
}

View File

@@ -1,56 +0,0 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Community } from '../../shared/community.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedBitstream } from './normalized-bitstream.model';
import { NormalizedCollection } from './normalized-collection.model';
import { Bitstream } from '../../shared/bitstream.model';
import { Collection } from '../../shared/collection.model';
/**
* Normalized model class for a DSpace Community
*/
@mapsTo(Community)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedCommunity extends NormalizedDSpaceObject<Community> {
/**
* A string representing the unique handle of this Community
*/
@autoserialize
handle: string;
/**
* The Bitstream that represents the logo of this Community
*/
@deserialize
@relationship(Bitstream, false)
logo: string;
/**
* An array of Communities that are direct parents of this Community
*/
@deserialize
@relationship(Community, true)
parents: string[];
/**
* The Community that owns this Community
*/
@deserialize
@relationship(Community, false)
owner: string;
/**
* List of Collections that are owned by this Community
*/
@deserialize
@relationship(Collection, true)
collections: string[];
@deserialize
@relationship(Community, true)
subcommunities: string[];
}

View File

@@ -1,72 +0,0 @@
import { autoserializeAs, deserializeAs, autoserialize } from 'cerialize';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models';
import { mapsTo } from '../builders/build-decorators';
import { NormalizedObject } from './normalized-object.model';
import { TypedObject } from '../object-cache.reducer';
/**
* An model class for a DSpaceObject.
*/
@mapsTo(DSpaceObject)
export class NormalizedDSpaceObject<T extends DSpaceObject> extends NormalizedObject<T> implements TypedObject {
/**
* The link to the rest endpoint where this object can be found
*
* Repeated here to make the serialization work,
* inheritSerialization doesn't seem to work for more than one level
*/
@deserializeAs(String)
self: string;
/**
* The human-readable identifier of this DSpaceObject
*
* Currently mapped to uuid but left in to leave room
* for a shorter, more user friendly type of id
*/
@autoserializeAs(String, 'uuid')
id: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserializeAs(String)
uuid: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
@autoserialize
type: string;
/**
* All metadata of this DSpaceObject
*/
@autoserializeAs(MetadataMapSerializer)
metadata: MetadataMap;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
@deserializeAs(String)
parents: string[];
/**
* The DSpaceObject that owns this DSpaceObject
*/
@deserializeAs(String)
owner: string;
/**
* The links to all related resources returned by the rest api.
*
* Repeated here to make the serialization work,
* inheritSerialization doesn't seem to work for more than one level
*/
@deserializeAs(Object)
_links: {
[name: string]: string
}
}

View File

@@ -1,42 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { NormalizedObject } from './normalized-object.model';
import { ExternalSourceEntry } from '../../shared/external-source-entry.model';
import { mapsTo } from '../builders/build-decorators';
import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models';
/**
* Normalized model class for an external source entry
*/
@mapsTo(ExternalSourceEntry)
@inheritSerialization(NormalizedObject)
export class NormalizedExternalSourceEntry extends NormalizedObject<ExternalSourceEntry> {
/**
* Unique identifier
*/
@autoserialize
id: string;
/**
* The value to display
*/
@autoserialize
display: string;
/**
* The value to store the entry with
*/
@autoserialize
value: string;
/**
* The ID of the external source this entry originates from
*/
@autoserialize
externalSource: string;
/**
* Metadata of the entry
*/
@autoserializeAs(MetadataMapSerializer)
metadata: MetadataMap;
}

View File

@@ -1,29 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedObject } from './normalized-object.model';
import { ExternalSource } from '../../shared/external-source.model';
import { mapsTo } from '../builders/build-decorators';
/**
* Normalized model class for an external source
*/
@mapsTo(ExternalSource)
@inheritSerialization(NormalizedObject)
export class NormalizedExternalSource extends NormalizedObject<ExternalSource> {
/**
* Unique identifier
*/
@autoserialize
id: string;
/**
* The name of this external source
*/
@autoserialize
name: string;
/**
* Is the source hierarchical?
*/
@autoserialize
hierarchical: boolean;
}

View File

@@ -1,72 +0,0 @@
import { inheritSerialization, deserialize, autoserialize, autoserializeAs } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Item } from '../../shared/item.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { Collection } from '../../shared/collection.model';
import { Relationship } from '../../shared/item-relationships/relationship.model';
import { Bundle } from '../../shared/bundle.model';
/**
* Normalized model class for a DSpace Item
*/
@mapsTo(Item)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedItem extends NormalizedDSpaceObject<Item> {
/**
* A string representing the unique handle of this Item
*/
@autoserialize
handle: string;
/**
* The Date of the last modification of this Item
*/
@deserialize
lastModified: Date;
/**
* A boolean representing if this Item is currently archived or not
*/
@autoserializeAs(Boolean, 'inArchive')
isArchived: boolean;
/**
* A boolean representing if this Item is currently discoverable or not
*/
@autoserializeAs(Boolean, 'discoverable')
isDiscoverable: boolean;
/**
* A boolean representing if this Item is currently withdrawn or not
*/
@autoserializeAs(Boolean, 'withdrawn')
isWithdrawn: boolean;
/**
* An array of Collections that are direct parents of this Item
*/
@deserialize
@relationship(Collection, true)
parents: string[];
/**
* The Collection that owns this Item
*/
@deserialize
@relationship(Collection, false)
owningCollection: string;
/**
* List of Bitstreams that are owned by this Item
*/
@deserialize
@relationship(Bundle, true)
bundles: string[];
@deserialize
@relationship(Relationship, true)
relationships: string[];
}

View File

@@ -1,24 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { mapsTo } from '../builders/build-decorators';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { License } from '../../shared/license.model';
/**
* Normalized model class for a Collection License
*/
@mapsTo(License)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedLicense extends NormalizedDSpaceObject<License> {
/**
* A boolean representing if this License is custom or not
*/
@autoserialize
custom: boolean;
/**
* The text of the license
*/
@autoserialize
text: string;
}

View File

@@ -1,24 +0,0 @@
import { CacheableObject, TypedObject } from '../object-cache.reducer';
import { autoserialize, deserialize } from 'cerialize';
import { ResourceType } from '../../shared/resource-type';
/**
* An abstract model class for a NormalizedObject.
*/
export abstract class NormalizedObject<T extends TypedObject> implements CacheableObject {
/**
* The link to the rest endpoint where this object can be found
*/
@deserialize
self: string;
@deserialize
_links: {
[name: string]: string
};
/**
* A string representing the kind of object
*/
@deserialize
type: string;
}

View File

@@ -1,48 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ResourcePolicy } from '../../shared/resource-policy.model';
import { mapsTo } from '../builders/build-decorators';
import { NormalizedObject } from './normalized-object.model';
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
import { ActionType } from './action-type.model';
/**
* Normalized model class for a Resource Policy
*/
@mapsTo(ResourcePolicy)
@inheritSerialization(NormalizedObject)
export class NormalizedResourcePolicy extends NormalizedObject<ResourcePolicy> {
/**
* The action that is allowed by this Resource Policy
*/
@autoserialize
action: ActionType;
/**
* The name for this Resource Policy
*/
@autoserialize
name: string;
/**
* The uuid of the Group this Resource Policy applies to
*/
@autoserialize
groupUUID: string;
/**
* Identifier for this Resource Policy
* Note that this ID is unique for resource policies,
* but might not be unique across different object types
*/
@autoserialize
id: string;
/**
* The universally unique identifier for this Resource Policy
* Consist of a prefix and the id field to ensure the identifier is unique across all object types
*/
@autoserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
uuid: string;
}

View File

@@ -1,13 +0,0 @@
import { inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { mapsTo } from '../builders/build-decorators';
import { Site } from '../../shared/site.model';
/**
* Normalized model class for a Site object
*/
@mapsTo(Site)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedSite extends NormalizedDSpaceObject<Site> {
}

View File

@@ -1,6 +1,6 @@
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { Operation } from 'fast-json-patch';
import { objectCacheReducer } from './object-cache.reducer'; import { Item } from '../shared/item.model';
import { import {
AddPatchObjectCacheAction, AddPatchObjectCacheAction,
AddToObjectCacheAction, AddToObjectCacheAction,
@@ -8,8 +8,8 @@ import {
RemoveFromObjectCacheAction, RemoveFromObjectCacheAction,
ResetObjectCacheTimestampsAction ResetObjectCacheTimestampsAction
} from './object-cache.actions'; } from './object-cache.actions';
import { Operation } from 'fast-json-patch';
import { Item } from '../shared/item.model'; import { objectCacheReducer } from './object-cache.reducer';
class NullAction extends RemoveFromObjectCacheAction { class NullAction extends RemoveFromObjectCacheAction {
type = null; type = null;
@@ -31,19 +31,21 @@ describe('objectCacheReducer', () => {
data: { data: {
type: Item.type, type: Item.type,
self: selfLink1, self: selfLink1,
foo: 'bar' foo: 'bar',
_links: { self: { href: selfLink1 } }
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000, msToLive: 900000,
requestUUID: requestUUID1, requestUUID: requestUUID1,
patches: [], patches: [],
isDirty: false isDirty: false,
}, },
[selfLink2]: { [selfLink2]: {
data: { data: {
type: Item.type, type: Item.type,
self: requestUUID2, self: requestUUID2,
foo: 'baz' foo: 'baz',
_links: { self: { href: requestUUID2 } }
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000, msToLive: 900000,
@@ -70,7 +72,7 @@ describe('objectCacheReducer', () => {
it('should add the payload to the cache in response to an ADD action', () => { it('should add the payload to the cache in response to an ADD action', () => {
const state = Object.create(null); const state = Object.create(null);
const objectToCache = { self: selfLink1, type: Item.type }; const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const requestUUID = requestUUID1; const requestUUID = requestUUID1;
@@ -87,7 +89,8 @@ describe('objectCacheReducer', () => {
self: selfLink1, self: selfLink1,
foo: 'baz', foo: 'baz',
somethingElse: true, somethingElse: true,
type: Item.type type: Item.type,
_links: { self: { href: selfLink1 } }
}; };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
@@ -103,7 +106,7 @@ describe('objectCacheReducer', () => {
it('should perform the ADD action without affecting the previous state', () => { it('should perform the ADD action without affecting the previous state', () => {
const state = Object.create(null); const state = Object.create(null);
const objectToCache = { self: selfLink1, type: Item.type }; const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const requestUUID = requestUUID1; const requestUUID = requestUUID1;
@@ -121,8 +124,8 @@ describe('objectCacheReducer', () => {
expect(newState[selfLink1]).toBeUndefined(); expect(newState[selfLink1]).toBeUndefined();
}); });
it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { it('shouldn\'t do anything in response to the REMOVE action for an object that isn\'t cached', () => {
const wrongKey = "this isn't cached"; const wrongKey = 'this isn\'t cached';
const action = new RemoveFromObjectCacheAction(wrongKey); const action = new RemoveFromObjectCacheAction(wrongKey);
const newState = objectCacheReducer(testState, action); const newState = objectCacheReducer(testState, action);

View File

@@ -1,3 +1,5 @@
import { HALLink } from '../shared/hal-link.model';
import { HALResource } from '../shared/hal-resource.model';
import { import {
ObjectCacheAction, ObjectCacheAction,
ObjectCacheActionTypes, ObjectCacheActionTypes,
@@ -10,13 +12,6 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheEntry } from './cache-entry'; import { CacheEntry } from './cache-entry';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { applyPatch, Operation } from 'fast-json-patch'; import { applyPatch, Operation } from 'fast-json-patch';
import { NormalizedItem } from './models/normalized-item.model';
export enum DirtyType {
Created = 'Created',
Updated = 'Updated',
Deleted = 'Deleted'
}
/** /**
* An interface to represent a JsonPatch * An interface to represent a JsonPatch
@@ -35,6 +30,7 @@ export interface Patch {
export abstract class TypedObject { export abstract class TypedObject {
static type: ResourceType; static type: ResourceType;
type: ResourceType;
} }
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -43,10 +39,13 @@ export abstract class TypedObject {
* *
* A cacheable object should have a self link * A cacheable object should have a self link
*/ */
export class CacheableObject extends TypedObject { export class CacheableObject extends TypedObject implements HALResource {
uuid?: string; uuid?: string;
handle?: string; handle?: string;
self: string;
_links: {
self: HALLink;
}
// isNew: boolean; // isNew: boolean;
// dirtyType: DirtyType; // dirtyType: DirtyType;
// hasDirtyAttributes: boolean; // hasDirtyAttributes: boolean;
@@ -131,9 +130,9 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
* the new state, with the object added, or overwritten. * the new state, with the object added, or overwritten.
*/ */
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
const existing = state[action.payload.objectToCache.self]; const existing = state[action.payload.objectToCache._links.self.href];
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.objectToCache.self]: { [action.payload.objectToCache._links.self.href]: {
data: action.payload.objectToCache, data: action.payload.objectToCache,
timeAdded: action.payload.timeAdded, timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive, msToLive: action.payload.msToLive,

View File

@@ -1,34 +1,36 @@
import * as ngrx from '@ngrx/store'; import * as ngrx from '@ngrx/store';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { first } from 'rxjs/operators';
import { ObjectCacheService } from './object-cache.service'; import { CoreState } from '../core.reducers';
import { RestRequestMethod } from '../data/rest-request-method';
import { Item } from '../shared/item.model';
import { import {
AddPatchObjectCacheAction, AddPatchObjectCacheAction,
AddToObjectCacheAction, AddToObjectCacheAction,
ApplyPatchObjectCacheAction, ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction RemoveFromObjectCacheAction
} from './object-cache.actions'; } from './object-cache.actions';
import { CoreState } from '../core.reducers';
import { NormalizedItem } from './models/normalized-item.model';
import { first } from 'rxjs/operators';
import { Operation } from 'fast-json-patch';
import { RestRequestMethod } from '../data/rest-request-method';
import { AddToSSBAction } from './server-sync-buffer.actions';
import { Patch } from './object-cache.reducer'; import { Patch } from './object-cache.reducer';
import { Item } from '../shared/item.model';
import { ObjectCacheService } from './object-cache.service';
import { AddToSSBAction } from './server-sync-buffer.actions';
describe('ObjectCacheService', () => { describe('ObjectCacheService', () => {
let service: ObjectCacheService; let service: ObjectCacheService;
let store: Store<CoreState>; let store: Store<CoreState>;
let linkServiceStub;
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736'; const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
let objectToCache = { let objectToCache = {
self: selfLink, type: Item.type,
type: Item.type _links: {
self: { href: selfLink }
}
}; };
let cacheEntry; let cacheEntry;
let invalidCacheEntry; let invalidCacheEntry;
@@ -36,8 +38,10 @@ describe('ObjectCacheService', () => {
function init() { function init() {
objectToCache = { objectToCache = {
self: selfLink, type: Item.type,
type: Item.type _links: {
self: { href: selfLink }
}
}; };
cacheEntry = { cacheEntry = {
data: objectToCache, data: objectToCache,
@@ -50,8 +54,12 @@ describe('ObjectCacheService', () => {
beforeEach(() => { beforeEach(() => {
init(); init();
store = new Store<CoreState>(undefined, undefined, undefined); store = new Store<CoreState>(undefined, undefined, undefined);
linkServiceStub = {
removeResolvedLinks: (a) => a
};
spyOn(linkServiceStub, 'removeResolvedLinks').and.callThrough();
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
service = new ObjectCacheService(store); service = new ObjectCacheService(store, linkServiceStub);
spyOn(Date.prototype, 'getTime').and.callFake(() => { spyOn(Date.prototype, 'getTime').and.callFake(() => {
return timestamp; return timestamp;
@@ -62,6 +70,7 @@ describe('ObjectCacheService', () => {
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => { it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
service.add(objectToCache, msToLive, requestUUID); service.add(objectToCache, msToLive, requestUUID);
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID)); expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID));
expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalledWith(objectToCache);
}); });
}); });
@@ -82,9 +91,9 @@ describe('ObjectCacheService', () => {
// due to the implementation of spyOn above, this subscribe will be synchronous // due to the implementation of spyOn above, this subscribe will be synchronous
service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => { service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => {
expect(o.self).toBe(selfLink); expect(o._links.self.href).toBe(selfLink);
// this only works if testObj is an instance of TestClass // this only works if testObj is an instance of TestClass
expect(o instanceof NormalizedItem).toBeTruthy(); expect(o instanceof Item).toBeTruthy();
} }
); );
}); });
@@ -105,13 +114,14 @@ describe('ObjectCacheService', () => {
describe('getList', () => { describe('getList', () => {
it('should return an observable of the array of cached objects with the specified self link and type', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => {
const item = new NormalizedItem(); const item = Object.assign(new Item(), {
item.self = selfLink; _links: { self: { href: selfLink } }
});
spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item)); spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item));
service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => { service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => {
expect(arr[0].self).toBe(selfLink); expect(arr[0]._links.self.href).toBe(selfLink);
expect(arr[0] instanceof NormalizedItem).toBeTruthy(); expect(arr[0] instanceof Item).toBeTruthy();
}); });
}); });
}); });
@@ -127,7 +137,7 @@ describe('ObjectCacheService', () => {
expect(service.hasBySelfLink(selfLink)).toBe(true); expect(service.hasBySelfLink(selfLink)).toBe(true);
}); });
it("should return false if the object with the supplied self link isn't cached", () => { it('should return false if the object with the supplied self link isn\'t cached', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => { spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => { return () => {
return () => observableOf(undefined); return () => observableOf(undefined);

View File

@@ -10,7 +10,7 @@ import { coreSelector } from '../core.selectors';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
import { selfLinkFromUuidSelector } from '../index/index.selectors'; import { selfLinkFromUuidSelector } from '../index/index.selectors';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedObject } from './models/normalized-object.model'; import { LinkService } from './builders/link.service';
import { import {
AddPatchObjectCacheAction, AddPatchObjectCacheAction,
AddToObjectCacheAction, AddToObjectCacheAction,
@@ -20,7 +20,7 @@ import {
import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
import { AddToSSBAction } from './server-sync-buffer.actions'; import { AddToSSBAction } from './server-sync-buffer.actions';
import { getMapsToType } from './builders/build-decorators'; import { getClassForType } from './builders/build-decorators';
/** /**
* The base selector function to select the object cache in the store * The base selector function to select the object cache in the store
@@ -45,21 +45,25 @@ const entryFromSelfLinkSelector =
*/ */
@Injectable() @Injectable()
export class ObjectCacheService { export class ObjectCacheService {
constructor(private store: Store<CoreState>) { constructor(
private store: Store<CoreState>,
private linkService: LinkService
) {
} }
/** /**
* Add an object to the cache * Add an object to the cache
* *
* @param objectToCache * @param object
* The object to add * The object to add
* @param msToLive * @param msToLive
* The number of milliseconds it should be cached for * The number of milliseconds it should be cached for
* @param requestUUID * @param requestUUID
* The UUID of the request that resulted in this object * The UUID of the request that resulted in this object
*/ */
add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void { add(object: CacheableObject, msToLive: number, requestUUID: string): void {
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID)); 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));
} }
/** /**
@@ -77,14 +81,14 @@ export class ObjectCacheService {
* *
* @param uuid * @param uuid
* The UUID of the object to get * The UUID of the object to get
* @return Observable<NormalizedObject<T>> * @return Observable<T>
* An observable of the requested object in normalized form * An observable of the requested object
*/ */
getObjectByUUID<T extends CacheableObject>(uuid: string): getObjectByUUID<T extends CacheableObject>(uuid: string):
Observable<NormalizedObject<T>> { Observable<T> {
return this.store.pipe( return this.store.pipe(
select(selfLinkFromUuidSelector(uuid)), select(selfLinkFromUuidSelector(uuid)),
mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) mergeMap((selfLink: string) => this.getObjectBySelfLink<T>(selfLink)
) )
) )
} }
@@ -94,10 +98,10 @@ export class ObjectCacheService {
* *
* @param selfLink * @param selfLink
* The selfLink of the object to get * The selfLink of the object to get
* @return Observable<NormalizedObject<T>> * @return Observable<T>
* An observable of the requested object in normalized form * An observable of the requested object
*/ */
getObjectBySelfLink<T extends CacheableObject>(selfLink: string): Observable<NormalizedObject<T>> { getObjectBySelfLink<T extends CacheableObject>(selfLink: string): Observable<T> {
return this.getBySelfLink(selfLink).pipe( return this.getBySelfLink(selfLink).pipe(
map((entry: ObjectCacheEntry) => { map((entry: ObjectCacheEntry) => {
if (isNotEmpty(entry.patches)) { if (isNotEmpty(entry.patches)) {
@@ -110,8 +114,11 @@ export class ObjectCacheService {
} }
), ),
map((entry: ObjectCacheEntry) => { map((entry: ObjectCacheEntry) => {
const type: GenericConstructor<NormalizedObject<T>> = getMapsToType((entry.data as any).type); const type: GenericConstructor<T> = getClassForType((entry.data as any).type);
return Object.assign(new type(), entry.data) as NormalizedObject<T> if (typeof type !== 'function') {
throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`);
}
return Object.assign(new type(), entry.data) as T
}) })
); );
} }
@@ -180,7 +187,7 @@ export class ObjectCacheService {
* The type of the objects to get * The type of the objects to get
* @return Observable<Array<T>> * @return Observable<Array<T>>
*/ */
getList<T extends CacheableObject>(selfLinks: string[]): Observable<Array<NormalizedObject<T>>> { getList<T extends CacheableObject>(selfLinks: string[]): Observable<T[]> {
return observableCombineLatest( return observableCombineLatest(
selfLinks.map((selfLink: string) => this.getObjectBySelfLink<T>(selfLink)) selfLinks.map((selfLink: string) => this.getObjectBySelfLink<T>(selfLink))
); );
@@ -254,7 +261,7 @@ export class ObjectCacheService {
const timeOutdated = entry.timeAdded + entry.msToLive; const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated; const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) { if (isOutDated) {
this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.self)); this.store.dispatch(new RemoveFromObjectCacheAction(entry.data._links.self.href));
} }
return !isOutDated; return !isOutDated;
} }

View File

@@ -1,4 +1,5 @@
import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; import { SearchQueryResponse } from '../../shared/search/search-query-response.model';
import { AuthStatus } from '../auth/models/auth-status.model';
import { RequestError } from '../data/request.models'; import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { ConfigObject } from '../config/models/config.model'; import { ConfigObject } from '../config/models/config.model';
@@ -11,7 +12,6 @@ import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstream
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { SubmissionObject } from '../submission/models/submission-object.model'; import { SubmissionObject } from '../submission/models/submission-object.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataSchema } from '../metadata/metadata-schema.model';
import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataField } from '../metadata/metadata-field.model';
import { ContentSource } from '../shared/content-source.model'; import { ContentSource } from '../shared/content-source.model';
@@ -203,7 +203,7 @@ export class AuthStatusResponse extends RestResponse {
public toCache = false; public toCache = false;
constructor( constructor(
public response: NormalizedAuthStatus, public response: AuthStatus,
public statusCode: number, public statusCode: number,
public statusText: string, public statusText: string,
) { ) {

View File

@@ -1,22 +1,22 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Observable, of as observableOf } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { Store, StoreModule } from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; import { Observable, of as observableOf } from 'rxjs';
import { GLOBAL_CONFIG } from '../../../config';
import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions';
import { RestRequestMethod } from '../data/rest-request-method';
import { Store, StoreModule } from '@ngrx/store';
import { RequestService } from '../data/request.service';
import { ObjectCacheService } from './object-cache.service';
import { MockStore } from '../../shared/testing/mock-store';
import * as operators from 'rxjs/operators'; import * as operators from 'rxjs/operators';
import { spyOnOperator } from '../../shared/testing/utils'; import { GLOBAL_CONFIG } from '../../../config';
import { DSpaceObject } from '../shared/dspace-object.model';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { MockStore } from '../../shared/testing/mock-store';
import { spyOnOperator } from '../../shared/testing/utils';
import { RequestService } from '../data/request.service';
import { RestRequestMethod } from '../data/rest-request-method';
import { DSpaceObject } from '../shared/dspace-object.model';
import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { ApplyPatchObjectCacheAction } from './object-cache.actions';
import { ObjectCacheService } from './object-cache.service';
import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions';
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
describe('ServerSyncBufferEffects', () => { describe('ServerSyncBufferEffects', () => {
let ssbEffects: ServerSyncBufferEffects; let ssbEffects: ServerSyncBufferEffects;
@@ -47,8 +47,9 @@ describe('ServerSyncBufferEffects', () => {
{ {
provide: ObjectCacheService, useValue: { provide: ObjectCacheService, useValue: {
getObjectBySelfLink: (link) => { getObjectBySelfLink: (link) => {
const object = new DSpaceObject(); const object = Object.assign(new DSpaceObject(), {
object.self = link; _links: { self: { href: link } }
});
return observableOf(object); return observableOf(object);
} }
} }

View File

@@ -2,6 +2,7 @@ import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { import {
AddToSSBAction, AddToSSBAction,
CommitSSBAction, CommitSSBAction,
@@ -18,7 +19,6 @@ import { RequestService } from '../data/request.service';
import { PutRequest } from '../data/request.models'; import { PutRequest } from '../data/request.models';
import { ObjectCacheService } from './object-cache.service'; import { ObjectCacheService } from './object-cache.service';
import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { ApplyPatchObjectCacheAction } from './object-cache.actions';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -100,7 +100,7 @@ export class ServerSyncBufferEffects {
return patchObject.pipe( return patchObject.pipe(
map((object) => { map((object) => {
const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object); const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject)); this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));

View File

@@ -1,22 +1,21 @@
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
import { ConfigResponseParsingService } from './config-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ConfigRequest } from '../data/request.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { ConfigRequest } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { NormalizedSubmissionSectionModel } from './models/normalized-config-submission-section.model'; import { ConfigResponseParsingService } from './config-response-parsing.service';
import { NormalizedSubmissionDefinitionModel } from './models/normalized-config-submission-definition.model'; import { SubmissionDefinitionModel } from './models/config-submission-definition.model';
import { SubmissionSectionModel } from './models/config-submission-section.model';
describe('ConfigResponseParsingService', () => { describe('ConfigResponseParsingService', () => {
let service: ConfigResponseParsingService; let service: ConfigResponseParsingService;
const EnvConfig = {} as GlobalConfig; const EnvConfig = {} as GlobalConfig;
const store = {} as Store<CoreState>; const store = {} as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store); const objectCacheService = new ObjectCacheService(store, undefined);
let validResponse; let validResponse;
beforeEach(() => { beforeEach(() => {
service = new ConfigResponseParsingService(EnvConfig, objectCacheService); service = new ConfigResponseParsingService(EnvConfig, objectCacheService);
@@ -150,7 +149,7 @@ describe('ConfigResponseParsingService', () => {
}, },
_embedded: [{}, {}], _embedded: [{}, {}],
_links: { _links: {
self: 'https://rest.api/config/submissiondefinitions/traditional/sections' self: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' }
} }
} }
} }
@@ -170,77 +169,76 @@ describe('ConfigResponseParsingService', () => {
totalElements: 4, totalElements: 4,
totalPages: 1, totalPages: 1,
currentPage: 1, currentPage: 1,
self: 'https://rest.api/config/submissiondefinitions/traditional/sections' _links: {
self: {
href: 'https://rest.api/config/submissiondefinitions/traditional/sections'
},
},
}); });
const definitions = const definitions =
Object.assign(new NormalizedSubmissionDefinitionModel(), { Object.assign(new SubmissionDefinitionModel(), {
isDefault: true, isDefault: true,
name: 'traditional', name: 'traditional',
type: 'submissiondefinition', type: 'submissiondefinition',
_links: { _links: {
sections: 'https://rest.api/config/submissiondefinitions/traditional/sections', sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' },
self: 'https://rest.api/config/submissiondefinitions/traditional' self: { href: 'https://rest.api/config/submissiondefinitions/traditional' }
}, },
self: 'https://rest.api/config/submissiondefinitions/traditional',
sections: new PaginatedList(pageinfo, [ sections: new PaginatedList(pageinfo, [
Object.assign(new NormalizedSubmissionSectionModel(), { Object.assign(new SubmissionSectionModel(), {
header: 'submit.progressbar.describe.stepone', header: 'submit.progressbar.describe.stepone',
mandatory: true, mandatory: true,
sectionType: 'submission-form', sectionType: 'submission-form',
visibility:{ visibility: {
main:null, main: null,
other:'READONLY' other: 'READONLY'
}, },
type: 'submissionsection', type: 'submissionsection',
_links: { _links: {
self: 'https://rest.api/config/submissionsections/traditionalpageone', self: { href: 'https://rest.api/config/submissionsections/traditionalpageone' },
config: 'https://rest.api/config/submissionforms/traditionalpageone' config: { href: 'https://rest.api/config/submissionforms/traditionalpageone' }
}, },
self: 'https://rest.api/config/submissionsections/traditionalpageone',
}), }),
Object.assign(new NormalizedSubmissionSectionModel(), { Object.assign(new SubmissionSectionModel(), {
header: 'submit.progressbar.describe.steptwo', header: 'submit.progressbar.describe.steptwo',
mandatory: true, mandatory: true,
sectionType: 'submission-form', sectionType: 'submission-form',
visibility:{ visibility: {
main:null, main: null,
other:'READONLY' other: 'READONLY'
}, },
type: 'submissionsection', type: 'submissionsection',
_links: { _links: {
self: 'https://rest.api/config/submissionsections/traditionalpagetwo', self: { href: 'https://rest.api/config/submissionsections/traditionalpagetwo' },
config: 'https://rest.api/config/submissionforms/traditionalpagetwo' config: { href: 'https://rest.api/config/submissionforms/traditionalpagetwo' }
}, },
self: 'https://rest.api/config/submissionsections/traditionalpagetwo',
}), }),
Object.assign(new NormalizedSubmissionSectionModel(), { Object.assign(new SubmissionSectionModel(), {
header: 'submit.progressbar.upload', header: 'submit.progressbar.upload',
mandatory: false, mandatory: false,
sectionType: 'upload', sectionType: 'upload',
visibility:{ visibility: {
main:null, main: null,
other:'READONLY' other: 'READONLY'
}, },
type: 'submissionsection', type: 'submissionsection',
_links: { _links: {
self: 'https://rest.api/config/submissionsections/upload', self: { href: 'https://rest.api/config/submissionsections/upload' },
config: 'https://rest.api/config/submissionuploads/upload' config: { href: 'https://rest.api/config/submissionuploads/upload' }
}, },
self: 'https://rest.api/config/submissionsections/upload',
}), }),
Object.assign(new NormalizedSubmissionSectionModel(), { Object.assign(new SubmissionSectionModel(), {
header: 'submit.progressbar.license', header: 'submit.progressbar.license',
mandatory: true, mandatory: true,
sectionType: 'license', sectionType: 'license',
visibility:{ visibility: {
main:null, main: null,
other:'READONLY' other: 'READONLY'
}, },
type: 'submissionsection', type: 'submissionsection',
_links: { _links: {
self: 'https://rest.api/config/submissionsections/license' self: { href: 'https://rest.api/config/submissionsections/license' }
}, },
self: 'https://rest.api/config/submissionsections/license',
}) })
]) ])
}); });

View File

@@ -15,6 +15,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
@Injectable() @Injectable()
export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected toCache = false; protected toCache = false;
protected shouldDirectlyAttachEmbeds = true;
constructor( constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,

View File

@@ -1,22 +1,40 @@
import { ConfigObject } from './config.model'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { SubmissionSectionModel } from './config-submission-section.model'; import { typedObject } from '../../cache/builders/build-decorators';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { SubmissionSectionModel } from './config-submission-section.model';
import { ConfigObject } from './config.model';
/** /**
* Class for the configuration describing the submission * Class for the configuration describing the submission
*/ */
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionDefinitionModel extends ConfigObject { export class SubmissionDefinitionModel extends ConfigObject {
static type = new ResourceType('submissiondefinition'); static type = new ResourceType('submissiondefinition');
/** /**
* A boolean representing if this submission definition is the default or not * A boolean representing if this submission definition is the default or not
*/ */
@autoserialize
isDefault: boolean; isDefault: boolean;
/** /**
* A list of SubmissionSectionModel that are present in this submission definition * A list of SubmissionSectionModel that are present in this submission definition
*/ */
// TODO refactor using remotedata
@deserialize
sections: PaginatedList<SubmissionSectionModel>; sections: PaginatedList<SubmissionSectionModel>;
/**
* The links to all related resources returned by the rest api.
*/
@deserialize
_links: {
self: HALLink,
collections: HALLink,
sections: HALLink
};
} }

View File

@@ -1,6 +1,10 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SubmissionDefinitionModel } from './config-submission-definition.model'; import { SubmissionDefinitionModel } from './config-submission-definition.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
@typedObject
@inheritSerialization(SubmissionDefinitionModel)
export class SubmissionDefinitionsModel extends SubmissionDefinitionModel { export class SubmissionDefinitionsModel extends SubmissionDefinitionModel {
static type = new ResourceType('submissiondefinitions'); static type = new ResourceType('submissiondefinitions');

View File

@@ -1,3 +1,5 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { ConfigObject } from './config.model'; import { ConfigObject } from './config.model';
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
@@ -12,11 +14,14 @@ export interface FormRowModel {
/** /**
* A model class for a NormalizedObject. * A model class for a NormalizedObject.
*/ */
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionFormModel extends ConfigObject { export class SubmissionFormModel extends ConfigObject {
static type = new ResourceType('submissionform'); static type = new ResourceType('submissionform');
/** /**
* An array of [FormRowModel] that are present in this form * An array of [FormRowModel] that are present in this form
*/ */
@autoserialize
rows: FormRowModel[]; rows: FormRowModel[];
} }

View File

@@ -1,9 +1,13 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SubmissionFormModel } from './config-submission-form.model'; import { SubmissionFormModel } from './config-submission-form.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
/** /**
* A model class for a NormalizedObject. * A model class for a NormalizedObject.
*/ */
@typedObject
@inheritSerialization(SubmissionFormModel)
export class SubmissionFormsModel extends SubmissionFormModel { export class SubmissionFormsModel extends SubmissionFormModel {
static type = new ResourceType('submissionforms'); static type = new ResourceType('submissionforms');
} }

View File

@@ -1,6 +1,9 @@
import { ConfigObject } from './config.model'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { SectionsType } from '../../../submission/sections/sections-type'; 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 { ResourceType } from '../../shared/resource-type';
import { ConfigObject } from './config.model';
/** /**
* An interface that define section visibility and its properties. * An interface that define section visibility and its properties.
@@ -10,27 +13,42 @@ export interface SubmissionSectionVisibility {
other: any other: any
} }
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionSectionModel extends ConfigObject { export class SubmissionSectionModel extends ConfigObject {
static type = new ResourceType('submissionsection'); static type = new ResourceType('submissionsection');
/** /**
* The header for this section * The header for this section
*/ */
@autoserialize
header: string; header: string;
/** /**
* A boolean representing if this submission section is the mandatory or not * A boolean representing if this submission section is the mandatory or not
*/ */
@autoserialize
mandatory: boolean; mandatory: boolean;
/** /**
* A string representing the kind of section object * A string representing the kind of section object
*/ */
@autoserialize
sectionType: SectionsType; sectionType: SectionsType;
/** /**
* The [SubmissionSectionVisibility] object for this section * The [SubmissionSectionVisibility] object for this section
*/ */
visibility: SubmissionSectionVisibility @autoserialize
visibility: SubmissionSectionVisibility;
/**
* The {@link HALLink}s for this SubmissionSectionModel
*/
@deserialize
_links: {
self: HALLink;
config: HALLink;
}
} }

View File

@@ -1,6 +1,10 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SubmissionSectionModel } from './config-submission-section.model'; import { SubmissionSectionModel } from './config-submission-section.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
@typedObject
@inheritSerialization(SubmissionSectionModel)
export class SubmissionSectionsModel extends SubmissionSectionModel { export class SubmissionSectionsModel extends SubmissionSectionModel {
static type = new ResourceType('submissionsections'); static type = new ResourceType('submissionsections');
} }

View File

@@ -1,22 +1,30 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { ConfigObject } from './config.model'; import { ConfigObject } from './config.model';
import { AccessConditionOption } from './config-access-condition-option.model'; import { AccessConditionOption } from './config-access-condition-option.model';
import { SubmissionFormsModel } from './config-submission-forms.model'; import { SubmissionFormsModel } from './config-submission-forms.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionUploadsModel extends ConfigObject { export class SubmissionUploadsModel extends ConfigObject {
static type = new ResourceType('submissionupload'); static type = new ResourceType('submissionupload');
/** /**
* A list of available bitstream access conditions * A list of available bitstream access conditions
*/ */
@autoserialize
accessConditionOptions: AccessConditionOption[]; accessConditionOptions: AccessConditionOption[];
/** /**
* An object representing the configuration describing the bistream metadata form * An object representing the configuration describing the bistream metadata form
*/ */
@autoserialize
metadata: SubmissionFormsModel; metadata: SubmissionFormsModel;
@autoserialize
required: boolean; required: boolean;
@autoserialize
maxSize: number; maxSize: number;
} }

View File

@@ -1,22 +1,30 @@
import { autoserialize, deserialize } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
export abstract class ConfigObject implements CacheableObject { export abstract class ConfigObject implements CacheableObject {
/** /**
* The name for this configuration * The name for this configuration
*/ */
@autoserialize
public name: string; public name: string;
/**
* The type of this ConfigObject
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/** /**
* The links to all related resources returned by the rest api. * The links to all related resources returned by the rest api.
*/ */
public _links: { @deserialize
[name: string]: string _links: {
self: HALLink,
[name: string]: HALLink
}; };
/**
* The link to the rest endpoint where this config object can be found
*/
self: string;
} }

View File

@@ -1,28 +0,0 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { SubmissionSectionModel } from './config-submission-section.model';
import { PaginatedList } from '../../data/paginated-list';
import { NormalizedConfigObject } from './normalized-config.model';
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
import { mapsTo } from '../../cache/builders/build-decorators';
import { SubmissionDefinitionModel } from './config-submission-definition.model';
/**
* Normalized class for the configuration describing the submission
*/
@mapsTo(SubmissionDefinitionModel)
@inheritSerialization(NormalizedConfigObject)
export class NormalizedSubmissionDefinitionModel extends NormalizedConfigObject<SubmissionDefinitionModel> {
/**
* A boolean representing if this submission definition is the default or not
*/
@autoserialize
isDefault: boolean;
/**
* A list of SubmissionSectionModel that are present in this submission definition
*/
@autoserializeAs(SubmissionSectionModel)
sections: PaginatedList<SubmissionSectionModel>;
}

View File

@@ -1,13 +0,0 @@
import { inheritSerialization } from 'cerialize';
import { NormalizedConfigObject } from './normalized-config.model';
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
import { mapsTo } from '../../cache/builders/build-decorators';
import { NormalizedSubmissionDefinitionModel } from './normalized-config-submission-definition.model';
/**
* Normalized class for the configuration describing the submission
*/
@mapsTo(SubmissionDefinitionsModel)
@inheritSerialization(NormalizedConfigObject)
export class NormalizedSubmissionDefinitionsModel extends NormalizedSubmissionDefinitionModel {
}

View File

@@ -1,18 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedConfigObject } from './normalized-config.model';
import { mapsTo } from '../../cache/builders/build-decorators';
import { FormRowModel, SubmissionFormModel } from './config-submission-form.model';
/**
* Normalized class for the configuration describing the submission form
*/
@mapsTo(SubmissionFormModel)
@inheritSerialization(NormalizedConfigObject)
export class NormalizedSubmissionFormModel extends NormalizedConfigObject<SubmissionFormModel> {
/**
* An array of [FormRowModel] that are present in this form
*/
@autoserialize
rows: FormRowModel[];
}

View File

@@ -1,12 +0,0 @@
import { inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { SubmissionFormsModel } from './config-submission-forms.model';
import { NormalizedSubmissionFormModel } from './normalized-config-submission-form.model';
/**
* Normalized class for the configuration describing the submission form
*/
@mapsTo(SubmissionFormsModel)
@inheritSerialization(NormalizedSubmissionFormModel)
export class NormalizedSubmissionFormsModel extends NormalizedSubmissionFormModel {
}

View File

@@ -1,41 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { SectionsType } from '../../../submission/sections/sections-type';
import { NormalizedConfigObject } from './normalized-config.model';
import {
SubmissionSectionModel,
SubmissionSectionVisibility
} from './config-submission-section.model';
import { mapsTo } from '../../cache/builders/build-decorators';
/**
* Normalized class for the configuration describing the submission section
*/
@mapsTo(SubmissionSectionModel)
@inheritSerialization(NormalizedConfigObject)
export class NormalizedSubmissionSectionModel extends NormalizedConfigObject<SubmissionSectionModel> {
/**
* The header for this section
*/
@autoserialize
header: string;
/**
* A boolean representing if this submission section is the mandatory or not
*/
@autoserialize
mandatory: boolean;
/**
* A string representing the kind of section object
*/
@autoserialize
sectionType: SectionsType;
/**
* The [SubmissionSectionVisibility] object for this section
*/
@autoserialize
visibility: SubmissionSectionVisibility
}

View File

@@ -1,18 +0,0 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { SectionsType } from '../../../submission/sections/sections-type';
import { NormalizedConfigObject } from './normalized-config.model';
import {
SubmissionSectionModel,
SubmissionSectionVisibility
} from './config-submission-section.model';
import { mapsTo } from '../../cache/builders/build-decorators';
import { SubmissionSectionsModel } from './config-submission-sections.model';
import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model';
/**
* Normalized class for the configuration describing the submission section
*/
@mapsTo(SubmissionSectionsModel)
@inheritSerialization(NormalizedSubmissionSectionModel)
export class NormalizedSubmissionSectionsModel extends NormalizedSubmissionSectionModel {
}

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