diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..5889e7a85c0d4d2e5e97b5e010d9070bfc386c56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +.git +node-modules +__build__ +__server_build__ +typings +tsd_typings +npm-debug.log +dist +coverage +.idea +*.iml +*.ngfactory.ts +*.css.shim.ts +*.scss.shim.ts +.DS_Store +webpack.records.json +npm-debug.log.* +morgan.log +yarn-error.log +*.css +package-lock.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f10164ebd034a85b7525ef8ab7a150348064867c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +# This image will be published as dspace/dspace-angular +# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details + +FROM node:8-alpine +WORKDIR /app +ADD . /app/ +EXPOSE 3000 + +RUN yarn install +CMD yarn run watch diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index b04e93ff71036aefffac430568b89b7759803123..12c2578d9ceb6cc855f027a47e4e9842ac85818f 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -2,7 +2,7 @@ <div *ngIf="subCollectionsRD?.hasSucceeded" @fadeIn> <h2>{{'community.sub-collection-list.head' | translate}}</h2> <ul> - <li *ngFor="let collection of subCollectionsRD?.payload"> + <li *ngFor="let collection of subCollectionsRD?.payload.page"> <p> <span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br> <span class="text-muted">{{collection.shortDescription}}</span> diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index fc697198963c0f341cc10133b44308e0e0349c83..aed2b69a306d486c55f0d8e89535efbbd0f33907 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -6,6 +6,7 @@ import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; import { fadeIn } from '../../shared/animations/fade'; +import { PaginatedList } from '../../core/data/paginated-list'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -15,7 +16,7 @@ import { fadeIn } from '../../shared/animations/fade'; }) export class CommunityPageSubCollectionListComponent implements OnInit { @Input() community: Community; - subCollectionsRDObs: Observable<RemoteData<Collection[]>>; + subCollectionsRDObs: Observable<RemoteData<PaginatedList<Collection>>>; ngOnInit(): void { this.subCollectionsRDObs = this.community.collections; diff --git a/src/app/+item-page/field-components/collections/collections.component.html b/src/app/+item-page/field-components/collections/collections.component.html index bb7ab63341bcce738d6fae63459c1aa77d6394f9..6e5f9a350cdba2846a0f9a20aebce38bae6baac9 100644 --- a/src/app/+item-page/field-components/collections/collections.component.html +++ b/src/app/+item-page/field-components/collections/collections.component.html @@ -1,4 +1,4 @@ -<ds-metadata-field-wrapper [label]="label | translate"> +<ds-metadata-field-wrapper *ngIf="hasSucceeded() | async" [label]="label | translate"> <div class="collections"> <a *ngFor="let collection of (collections |Â async); let last=last;" [routerLink]="['/collections', collection.id]"> <span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span> diff --git a/src/app/+item-page/field-components/collections/collections.component.spec.ts b/src/app/+item-page/field-components/collections/collections.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..871018a9d843149b7b83fb7a794821a6f3d7e4e7 --- /dev/null +++ b/src/app/+item-page/field-components/collections/collections.component.spec.ts @@ -0,0 +1,74 @@ +import { CollectionsComponent } from './collections.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +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 { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { TranslateModule } from '@ngx-translate/core'; + +let collectionsComponent: CollectionsComponent; +let fixture: ComponentFixture<CollectionsComponent>; + +const mockCollection1: Collection = Object.assign(new Collection(), { + metadata: [ + { + key: 'dc.description.abstract', + language: 'en_US', + value: 'Short description' + }] +}); + +const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: Observable.of(new RemoteData(false, false, true, null, mockCollection1))}); +const failedMockItem: Item = Object.assign(new Item(), {owningCollection: Observable.of(new RemoteData(false, false, false, null, mockCollection1))}); + +describe('CollectionsComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ CollectionsComponent ], + providers: [ + { provide: RemoteDataBuildService, useValue: getMockRemoteDataBuildService()} + ], + + schemas: [ NO_ERRORS_SCHEMA ] + }).overrideComponent(CollectionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(CollectionsComponent); + collectionsComponent = fixture.componentInstance; + collectionsComponent.label = 'test.test'; + collectionsComponent.separator = '<br/>'; + + })); + + describe('When the requested item request has succeeded', () => { + beforeEach(() => { + collectionsComponent.item = succeededMockItem; + fixture.detectChanges(); + }); + + it('should show the collection', () => { + const collectionField = fixture.debugElement.query(By.css('ds-metadata-field-wrapper div.collections')); + expect(collectionField).not.toBeNull(); + }); + }); + + describe('When the requested item request has failed', () => { + beforeEach(() => { + collectionsComponent.item = failedMockItem; + fixture.detectChanges(); + }); + + it('should not show the collection', () => { + const collectionField = fixture.debugElement.query(By.css('ds-metadata-field-wrapper div.collections')); + expect(collectionField).toBeNull(); + }); + }); +}); diff --git a/src/app/+item-page/field-components/collections/collections.component.ts b/src/app/+item-page/field-components/collections/collections.component.ts index 8b7b5d7f58122fb1fa67c65a4ab253af73de8729..83bb0d464d4d0f78e44c2d8cd485d26e8830157e 100644 --- a/src/app/+item-page/field-components/collections/collections.component.ts +++ b/src/app/+item-page/field-components/collections/collections.component.ts @@ -38,4 +38,8 @@ export class CollectionsComponent implements OnInit { this.collections = this.item.owner.map((rd: RemoteData<Collection>) => [rd.payload]); } + hasSucceeded() { + return this.item.owner.map((rd: RemoteData<Collection>) => rd.hasSucceeded); + } + } diff --git a/src/app/+search-page/paginated-search-options.model.ts b/src/app/+search-page/paginated-search-options.model.ts index 0c403af827c54ace7cc00a102e7ac9f909b3e280..4f04480391527a0108d36241e305ecc806b8beea 100644 --- a/src/app/+search-page/paginated-search-options.model.ts +++ b/src/app/+search-page/paginated-search-options.model.ts @@ -1,7 +1,6 @@ import { SortOptions } from '../core/cache/models/sort-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { isNotEmpty } from '../shared/empty.util'; -import { URLCombiner } from '../core/url-combiner/url-combiner'; import { SearchOptions } from './search-options.model'; export class PaginatedSearchOptions extends SearchOptions { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index d576b9ea3299786051fa22e3265d71d03cc7ebb0..d934f60e486f24a7471106d4e1ccee4da43c6ae5 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -8,12 +8,14 @@ import { RemoteDataError } from '../../data/remote-data-error'; import { GetRequest } from '../../data/request.models'; import { RequestEntry } from '../../data/request.reducer'; import { RequestService } from '../../data/request.service'; + import { NormalizedObject } from '../models/normalized-object.model'; import { ObjectCacheService } from '../object-cache.service'; import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models'; import { ResponseCacheEntry } from '../response-cache.reducer'; import { ResponseCacheService } from '../response-cache.service'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; +import { PageInfo } from '../../shared/page-info.model'; import { getRequestFromSelflink, getResourceLinksFromResponse, @@ -96,7 +98,6 @@ export class RemoteDataBuildService { error = new RemoteDataError(resEntry.response.statusCode, errorMessage); } } - return new RemoteData( requestPending, responsePending, @@ -107,7 +108,7 @@ export class RemoteDataBuildService { }); } - buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> { + buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> { if (typeof href$ === 'string') { href$ = Observable.of(href$); } @@ -144,11 +145,7 @@ export class RemoteDataBuildService { ); const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => { - if (hasValue(pageInfo)) { - return new PaginatedList(pageInfo, tDomainList); - } else { - return tDomainList; - } + return new PaginatedList(pageInfo, tDomainList); }); return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); @@ -160,35 +157,43 @@ export class RemoteDataBuildService { const relationships = getRelationships(normalized.constructor) || []; relationships.forEach((relationship: string) => { + let result; if (hasValue(normalized[relationship])) { const { resourceType, isList } = getRelationMetadata(normalized, relationship); - if (Array.isArray(normalized[relationship])) { - normalized[relationship].forEach((href: string) => { + 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 = []; - normalized[relationship].forEach((href: string) => { + objectList.forEach((href: string) => { rdArr.push(this.buildSingle(href)); }); if (isList) { - links[relationship] = this.aggregate(rdArr); + result = this.aggregate(rdArr); } else if (rdArr.length === 1) { - links[relationship] = rdArr[0]; + result = rdArr[0]; } } else { - this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), normalized[relationship])); + 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) { - links[relationship] = this.buildList(normalized[relationship]); + result = this.buildList(objectList); } else { - links[relationship] = this.buildSingle(normalized[relationship]); + result = this.buildSingle(objectList); } } + + if (hasValue(normalized[relationship].page)) { + links[relationship] = this.aggregatePaginatedList(result, normalized[relationship].pageInfo); + } else { + links[relationship] = result; + } } }); @@ -249,4 +254,8 @@ export class RemoteDataBuildService { }) } + aggregatePaginatedList<T>(input: Observable<RemoteData<T[]>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> { + return input.map((rd) => Object.assign(rd, {payload: new PaginatedList(pageInfo, rd.payload)})); + } + } diff --git a/src/app/core/cache/id-to-uuid-serializer.spec.ts b/src/app/core/cache/id-to-uuid-serializer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7f6929dddeef6a82104ae12b8eec0b5004479d5 --- /dev/null +++ b/src/app/core/cache/id-to-uuid-serializer.spec.ts @@ -0,0 +1,34 @@ +import { IDToUUIDSerializer } from './id-to-uuid-serializer'; + +describe('IDToUUIDSerializer', () => { + let serializer: IDToUUIDSerializer; + const prefix = 'test-prefix'; + + beforeEach(() => { + serializer = new IDToUUIDSerializer(prefix); + }); + + describe('Serialize', () => { + it('should return undefined', () => { + expect(serializer.Serialize('some-uuid')).toBeUndefined() + }); + }); + + describe('Deserialize', () => { + describe('when ID is defined', () => { + it('should prepend the prefix to the ID', () => { + const id = 'some-id'; + expect(serializer.Deserialize(id)).toBe(`${prefix}-${id}`); + }); + }); + + describe('when ID is null or undefined', () => { + it('should return null or undefined', () => { + expect(serializer.Deserialize(null)).toBeNull(); + expect(serializer.Deserialize(undefined)).toBeUndefined(); + }); + }); + + }); + +}); diff --git a/src/app/core/cache/id-to-uuid-serializer.ts b/src/app/core/cache/id-to-uuid-serializer.ts new file mode 100644 index 0000000000000000000000000000000000000000..79576d448e2ae867936dcdf50d1e10d839c48548 --- /dev/null +++ b/src/app/core/cache/id-to-uuid-serializer.ts @@ -0,0 +1,35 @@ +import { hasValue } from '../../shared/empty.util'; + +/** + * Serializer to create unique fake UUID's from id's that might otherwise be the same across multiple object types + */ +export class IDToUUIDSerializer { + /** + * @param {string} prefix To prepend the original ID with + */ + constructor(private prefix: string) { + } + + /** + * Method to serialize a UUID + * @param {string} uuid + * @returns {any} undefined Fake UUID's should not be sent back to the server, but only be used in the UI + */ + Serialize(uuid: string): any { + return undefined; + } + + /** + * Method to deserialize a UUID + * @param {string} id Identifier to transform in to a UUID + * @returns {string} UUID based on the prefix and the given id + */ + Deserialize(id: string): string { + if (hasValue(id)) { + return `${this.prefix}-${id}`; + } else { + return id; + } + + } +} diff --git a/src/app/core/cache/models/action-type.model.ts b/src/app/core/cache/models/action-type.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4965f93e8935d2c871c29c0fe6403ff3ad23ce58 --- /dev/null +++ b/src/app/core/cache/models/action-type.model.ts @@ -0,0 +1,64 @@ +/** + * Enum representing the Action Type of a Resource Policy + */ +export enum ActionType { + /** + * Action of reading, viewing or downloading something + */ + READ = 0, + + /** + * Action of modifying something + */ + WRITE = 1, + + /** + * Action of deleting something + */ + DELETE = 2, + + /** + * Action of adding something to a container + */ + ADD = 3, + + /** + * Action of removing something from a container + */ + REMOVE = 4, + + /** + * Action of performing workflow step 1 + */ + WORKFLOW_STEP_1 = 5, + + /** + * Action of performing workflow step 2 + */ + WORKFLOW_STEP_2 = 6, + + /** + * Action of performing workflow step 3 + */ + WORKFLOW_STEP_3 = 7, + + /** + * Action of performing a workflow abort + */ + WORKFLOW_ABORT = 8, + + /** + * Default Read policies for Bitstreams submitted to container + */ + DEFAULT_BITSTREAM_READ = 9, + + /** + * Default Read policies for Items submitted to container + */ + DEFAULT_ITEM_READ = 10, + + /** + * Administrative actions + */ + ADMIN = 11, +} diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d11c97107661f46a58b1135b1c2d263945179e0 --- /dev/null +++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts @@ -0,0 +1,66 @@ +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 { SupportLevel } from './support-level.model'; + +/** + * Normalized model class for a Bitstream Format + */ +@mapsTo(BitstreamFormat) +@inheritSerialization(NormalizedObject) +export class NormalizedBitstreamFormat extends NormalizedObject { + + /** + * 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: SupportLevel; + + /** + * 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; +} diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index db8002a87432121a400bfac0482a363aea6e2505..63f84add41b29cdea28d4bfce13befb0c76b5ec9 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -5,6 +5,9 @@ import { Bitstream } from '../../shared/bitstream.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +/** + * Normalized model class for a DSpace Bitstream + */ @mapsTo(Bitstream) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedBitstream extends NormalizedDSpaceObject { diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts index 3b594dd30856ebad6a2e0cbffded72a80eef1b58..5535ab57e5df13161109b50ed5869e369a0a7a04 100644 --- a/src/app/core/cache/models/normalized-bundle.model.ts +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -5,6 +5,9 @@ import { Bundle } from '../../shared/bundle.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +/** + * Normalized model class for a DSpace Bundle + */ @mapsTo(Bundle) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedBundle extends NormalizedDSpaceObject { @@ -25,6 +28,9 @@ export class NormalizedBundle extends NormalizedDSpaceObject { */ owner: string; + /** + * List of Bitstreams that are part of this Bundle + */ @autoserialize @relationship(ResourceType.Bitstream, true) bitstreams: string[]; diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index 22e0d20eaad54c53ac0a1897653903a79eca03ab..a2c634c3e5a67161b1cb2bfb074c8c800c7b5634 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -1,10 +1,13 @@ -import { autoserialize, inheritSerialization, autoserializeAs } from 'cerialize'; +import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Collection } from '../../shared/collection.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +/** + * Normalized model class for a DSpace Collection + */ @mapsTo(Collection) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedCollection extends NormalizedDSpaceObject { @@ -36,6 +39,9 @@ export class NormalizedCollection extends NormalizedDSpaceObject { @relationship(ResourceType.Community, false) owner: string; + /** + * List of Items that are part of (not necessarily owned by) this Collection + */ @autoserialize @relationship(ResourceType.Item, true) items: string[]; diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index 03784e414b07246e69e032f46f7ecb630ba7ad46..b1c2fe3cdde75757ed0d124c0f903b6fb2f794c5 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -1,10 +1,13 @@ -import { autoserialize, inheritSerialization, autoserializeAs } from 'cerialize'; +import { autoserialize, 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'; +/** + * Normalized model class for a DSpace Community + */ @mapsTo(Community) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedCommunity extends NormalizedDSpaceObject { @@ -36,6 +39,9 @@ export class NormalizedCommunity extends NormalizedDSpaceObject { @relationship(ResourceType.Community, false) owner: string; + /** + * List of Collections that are owned by this Community + */ @autoserialize @relationship(ResourceType.Collection, true) collections: string[]; diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index a4a14e424c8ccd487552c9f93418bf5e50d33e2e..7d518bd0485f7736bda2323ee6663a3f025c1f3c 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -5,6 +5,9 @@ import { Item } from '../../shared/item.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +/** + * Normalized model class for a DSpace Item + */ @mapsTo(Item) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedItem extends NormalizedDSpaceObject { @@ -49,9 +52,13 @@ export class NormalizedItem extends NormalizedDSpaceObject { /** * The Collection that owns this Item */ + @autoserialize @relationship(ResourceType.Collection, false) owningCollection: string; + /** + * List of Bitstreams that are owned by this Item + */ @autoserialize @relationship(ResourceType.Bitstream, true) bitstreams: string[]; diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 5b13d55ac80bd5883a9ed4bc125744c8ba91137c..df67a1f2ce78e044fc216808e336c04414449c24 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -6,6 +6,8 @@ import { GenericConstructor } from '../../shared/generic-constructor'; import { NormalizedCommunity } from './normalized-community.model'; import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from './normalized-object.model'; +import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; +import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> { @@ -25,6 +27,12 @@ export class NormalizedObjectFactory { case ResourceType.Community: { return NormalizedCommunity } + case ResourceType.BitstreamFormat: { + return NormalizedBitstreamFormat + } + case ResourceType.ResourcePolicy: { + return NormalizedResourcePolicy + } default: { return undefined; } diff --git a/src/app/core/cache/models/normalized-resource-policy.model.ts b/src/app/core/cache/models/normalized-resource-policy.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..b767ca6491e3bec2ee39ad3423ec57eca29ed553 --- /dev/null +++ b/src/app/core/cache/models/normalized-resource-policy.model.ts @@ -0,0 +1,49 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { ResourcePolicy } from '../../shared/resource-policy.model'; + +import { mapsTo, relationship } from '../builders/build-decorators'; +import { NormalizedObject } from './normalized-object.model'; +import { IDToUUIDSerializer } from '../id-to-uuid-serializer'; +import { ResourceType } from '../../shared/resource-type'; +import { ActionType } from './action-type.model'; + +/** + * Normalized model class for a Resource Policy + */ +@mapsTo(ResourcePolicy) +@inheritSerialization(NormalizedObject) +export class NormalizedResourcePolicy extends NormalizedObject { + + /** + * The action that is allowed by this Resource Policy + */ + action: ActionType; + + /** + * The name for this Resource Policy + */ + @autoserialize + name: string; + + /** + * The uuid of the Group this Resource Policy applies to + */ + @relationship(ResourceType.Group, false) + @autoserializeAs(String, 'groupUUID') + group: 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; +} diff --git a/src/app/core/cache/models/support-level.model.ts b/src/app/core/cache/models/support-level.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..30f759d55ffa23eeb07fba8469ced22ba116a725 --- /dev/null +++ b/src/app/core/cache/models/support-level.model.ts @@ -0,0 +1,19 @@ +/** + * Enum representing the Support Level of a Bitstream Format + */ +export enum SupportLevel { + /** + * Unknown for Bitstream Formats that are unknown to the system + */ + Unknown = 0, + + /** + * Unknown for Bitstream Formats that are known to the system, but not fully supported + */ + Known = 1, + + /** + * Supported for Bitstream Formats that are known to the system and fully supported + */ + Supported = 2, +} diff --git a/src/app/core/cache/response-cache.service.spec.ts b/src/app/core/cache/response-cache.service.spec.ts index 1def7faa02d0a3ddb4ce91ecb6cf32ce51386091..77838b6eb6d4d2c62ba8611cb38749a53fa9ad97 100644 --- a/src/app/core/cache/response-cache.service.spec.ts +++ b/src/app/core/cache/response-cache.service.spec.ts @@ -46,7 +46,6 @@ describe('ResponseCacheService', () => { let testObj: ResponseCacheEntry; service.get(keys[1]).first().subscribe((entry) => { - console.log(entry); testObj = entry; }); expect(testObj.key).toEqual(keys[1]); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index bde08579460cc2cef3725583eaa2c1d2fd353473..050b3c2da591483f3f2105db0c32d78213d5d4fa 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -4,8 +4,9 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { PageInfo } from '../shared/page-info.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; import { GenericConstructor } from '../shared/generic-constructor'; +import { PaginatedList } from './paginated-list'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; function isObjectLevel(halObj: any) { return isNotEmpty(halObj._links) && hasValue(halObj._links.self); @@ -17,96 +18,103 @@ function isPaginatedResponse(halObj: any) { /* tslint:disable:max-classes-per-file */ -class ProcessRequestDTO<ObjectDomain> { - [key: string]: ObjectDomain[] -} - export abstract class BaseResponseParsingService { protected abstract EnvConfig: GlobalConfig; protected abstract objectCache: ObjectCacheService; protected abstract objectFactory: any; protected abstract toCache: boolean; - protected process<ObjectDomain,ObjectType>(data: any, requestHref: string): ProcessRequestDTO<ObjectDomain> { + protected process<ObjectDomain, ObjectType>(data: any, requestHref: string): any { if (isNotEmpty(data)) { - if (isPaginatedResponse(data)) { - return this.process(data._embedded, requestHref); + if (hasNoValue(data) || (typeof data !== 'object')) { + return data; + } else if (isPaginatedResponse(data)) { + return this.processPaginatedList(data, requestHref); + } else if (Array.isArray(data)) { + return this.processArray(data, requestHref); } else if (isObjectLevel(data)) { - return { topLevel: this.deserializeAndCache(data, requestHref) }; - } else { - const result = new ProcessRequestDTO<ObjectDomain>(); - if (Array.isArray(data)) { - result.topLevel = []; - data.forEach((datum) => { - if (isPaginatedResponse(datum)) { - const obj = this.process(datum, requestHref); - result.topLevel = [...result.topLevel, ...this.flattenSingleKeyObject(obj)]; - } else { - result.topLevel = [...result.topLevel, ...this.deserializeAndCache<ObjectDomain,ObjectType>(datum, requestHref)]; - } - }); - } else { - Object.keys(data) - .filter((property) => data.hasOwnProperty(property)) - .filter((property) => hasValue(data[property])) + const object = this.deserialize(data); + if (isNotEmpty(data._embedded)) { + Object + .keys(data._embedded) + .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { - if (isPaginatedResponse(data[property])) { - const obj = this.process(data[property], requestHref); - result[property] = this.flattenSingleKeyObject(obj); - } else { - result[property] = this.deserializeAndCache(data[property], requestHref); + const parsedObj = this.process<ObjectDomain, ObjectType>(data._embedded[property], requestHref); + if (isNotEmpty(parsedObj)) { + if (isPaginatedResponse(data._embedded[property])) { + object[property] = parsedObj; + object[property].page = parsedObj.page.map((obj) => obj.self); + } else if (isObjectLevel(data._embedded[property])) { + object[property] = parsedObj.self; + } else if (Array.isArray(parsedObj)) { + object[property] = parsedObj.map((obj) => obj.self) + } } }); } - return result; + this.cache(object, requestHref); + return object; } + const result = {}; + Object.keys(data) + .filter((property) => data.hasOwnProperty(property)) + .filter((property) => hasValue(data[property])) + .forEach((property) => { + const obj = this.process(data[property], requestHref); + result[property] = obj; + }); + return result; + } } - protected deserializeAndCache<ObjectDomain,ObjectType>(obj, requestHref: string): ObjectDomain[] { - if (Array.isArray(obj)) { - let result = []; - obj.forEach((o) => result = [...result, ...this.deserializeAndCache<ObjectDomain,ObjectType>(o, requestHref)]); - return result; + protected processPaginatedList<ObjectDomain, ObjectType>(data: any, requestHref: string): PaginatedList<ObjectDomain> { + const pageInfo: PageInfo = this.processPageInfo(data); + let list = data._embedded; + + // Workaround for inconsistency in rest response. Issue: https://github.com/DSpace/dspace-angular/issues/238 + if (!Array.isArray(list)) { + list = this.flattenSingleKeyObject(list); } + const page: ObjectDomain[] = this.processArray(list, requestHref); + return new PaginatedList<ObjectDomain>(pageInfo, page); + } + + protected processArray<ObjectDomain, ObjectType>(data: any, requestHref: string): ObjectDomain[] { + let array: ObjectDomain[] = []; + data.forEach((datum) => { + array = [...array, this.process(datum, requestHref)]; + } + ); + return array; + } + protected deserialize<ObjectDomain, ObjectType>(obj): any { const type: ObjectType = obj.type; if (hasValue(type)) { const normObjConstructor = this.objectFactory.getConstructor(type) as GenericConstructor<ObjectDomain>; if (hasValue(normObjConstructor)) { const serializer = new DSpaceRESTv2Serializer(normObjConstructor); - - let processed; - if (isNotEmpty(obj._embedded)) { - processed = this.process<ObjectDomain,ObjectType>(obj._embedded, requestHref); - } - const normalizedObj: any = serializer.deserialize(obj); - - if (isNotEmpty(processed)) { - const processedList = {}; - Object.keys(processed).forEach((key) => { - processedList[key] = processed[key].map((no: NormalizedObject) => (this.toCache) ? no.self : no); - }); - Object.assign(normalizedObj, processedList); - } - - if (this.toCache) { - this.addToObjectCache(normalizedObj, requestHref); - } - return [normalizedObj] as any; - + const res = serializer.deserialize(obj); + return res; } else { // TODO: move check to Validator? // throw new Error(`The server returned an object with an unknown a known type: ${type}`); - return []; + return null; } } else { // TODO: move check to Validator // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`); - return []; + return null; + } + } + + protected cache<ObjectDomain, ObjectType>(obj, requestHref) { + if (this.toCache) { + this.addToObjectCache(obj, requestHref); } } @@ -119,7 +127,7 @@ export abstract class BaseResponseParsingService { processPageInfo(payload: any): PageInfo { if (isNotEmpty(payload.page)) { - const pageObj = Object.assign({}, payload.page, {_links: payload._links}); + const pageObj = Object.assign({}, payload.page, { _links: payload._links }); const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); if (pageInfoObject.currentPage >= 0) { Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index 3a09de6e4c4e69a31637f4cac9d232bee1225fa7..654ee536518c85f268d459c3f10c3d61fc087387 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -1,5 +1,4 @@ import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; @@ -8,7 +7,8 @@ import { ConfigRequest } from './request.models'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model'; -import { SubmissionSectionModel } from '../shared/config/config-submission-section.model'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; describe('ConfigResponseParsingService', () => { let service: ConfigResponseParsingService; @@ -16,141 +16,143 @@ describe('ConfigResponseParsingService', () => { const EnvConfig = {} as GlobalConfig; const store = {} as Store<CoreState>; const objectCacheService = new ObjectCacheService(store); - + let validResponse; beforeEach(() => { service = new ConfigResponseParsingService(EnvConfig, objectCacheService); - }); - - describe('parse', () => { - const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional'); - - const validResponse = { + validResponse = { payload: { - id:'traditional', - name:'traditional', - type:'submissiondefinition', - isDefault:true, - _links:{ - sections:{ - href:'https://rest.api/config/submissiondefinitions/traditional/sections' - },self:{ - href:'https://rest.api/config/submissiondefinitions/traditional' + id: 'traditional', + name: 'traditional', + type: 'submissiondefinition', + isDefault: true, + _links: { + sections: { + href: 'https://rest.api/config/submissiondefinitions/traditional/sections' + }, + self: { + href: 'https://rest.api/config/submissiondefinitions/traditional' } }, - _embedded:{ - sections:{ - page:{ - number:0, - size:4, - totalPages:1,totalElements:4 + _embedded: { + sections: { + page: { + number: 0, + size: 4, + totalPages: 1, totalElements: 4 }, - _embedded:[ + _embedded: [ { - id:'traditionalpageone',header:'submit.progressbar.describe.stepone', - mandatory:true, - sectionType:'submission-form', - visibility:{ - main:null, - other:'READONLY' + id: 'traditionalpageone', header: 'submit.progressbar.describe.stepone', + mandatory: true, + sectionType: 'submission-form', + visibility: { + main: null, + other: 'READONLY' }, - type:'submissionsection', - _links:{ - self:{ - href:'https://rest.api/config/submissionsections/traditionalpageone' + type: 'submissionsection', + _links: { + self: { + href: 'https://rest.api/config/submissionsections/traditionalpageone' }, - config:{ - href:'https://rest.api/config/submissionforms/traditionalpageone' + config: { + href: 'https://rest.api/config/submissionforms/traditionalpageone' } } }, { - id:'traditionalpagetwo', - header:'submit.progressbar.describe.steptwo', - mandatory:true, - sectionType:'submission-form', - visibility:{ - main:null, - other:'READONLY' + id: 'traditionalpagetwo', + header: 'submit.progressbar.describe.steptwo', + mandatory: true, + sectionType: 'submission-form', + visibility: { + main: null, + other: 'READONLY' }, - type:'submissionsection', - _links:{ - self:{ - href:'https://rest.api/config/submissionsections/traditionalpagetwo' + type: 'submissionsection', + _links: { + self: { + href: 'https://rest.api/config/submissionsections/traditionalpagetwo' }, - config:{ - href:'https://rest.api/config/submissionforms/traditionalpagetwo' + config: { + href: 'https://rest.api/config/submissionforms/traditionalpagetwo' } } }, { - id:'upload', - header:'submit.progressbar.upload', - mandatory:false, - sectionType:'upload', - visibility:{ - main:null, - other:'READONLY' + id: 'upload', + header: 'submit.progressbar.upload', + mandatory: false, + sectionType: 'upload', + visibility: { + main: null, + other: 'READONLY' }, - type:'submissionsection', - _links:{ - self:{ - href:'https://rest.api/config/submissionsections/upload' + type: 'submissionsection', + _links: { + self: { + href: 'https://rest.api/config/submissionsections/upload' }, config: { - href:'https://rest.api/config/submissionuploads/upload' + href: 'https://rest.api/config/submissionuploads/upload' } } }, { - id:'license', - header:'submit.progressbar.license', - mandatory:true, - sectionType:'license', - visibility:{ - main:null, - other:'READONLY' + id: 'license', + header: 'submit.progressbar.license', + mandatory: true, + sectionType: 'license', + visibility: { + main: null, + other: 'READONLY' }, - type:'submissionsection', - _links:{ - self:{ - href:'https://rest.api/config/submissionsections/license' + type: 'submissionsection', + _links: { + self: { + href: 'https://rest.api/config/submissionsections/license' } } } ], - _links:{ - self:'https://rest.api/config/submissiondefinitions/traditional/sections' + _links: { + self: { + href: 'https://rest.api/config/submissiondefinitions/traditional/sections' + } } } } }, - statusCode:'200' + statusCode: '200' }; + }); + + describe('parse', () => { + const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional'); const invalidResponse1 = { payload: {}, - statusCode:'200' + statusCode: '200' }; const invalidResponse2 = { payload: { - id:'traditional', - name:'traditional', - type:'submissiondefinition', - isDefault:true, - _links:{}, - _embedded:{ - sections:{ - page:{ - number:0, - size:4, - totalPages:1,totalElements:4 + id: 'traditional', + name: 'traditional', + type: 'submissiondefinition', + isDefault: true, + _links: {}, + _embedded: { + sections: { + page: { + number: 0, + size: 4, + totalPages: 1, totalElements: 4 }, - _embedded:[{},{}], - _links:{ - self:'https://rest.api/config/submissiondefinitions/traditional/sections' + _embedded: [{}, {}], + _links: { + self: 'https://rest.api/config/submissiondefinitions/traditional/sections' } } } }, - statusCode:'200' + statusCode: '200' }; const invalidResponse3 = { @@ -159,61 +161,24 @@ describe('ConfigResponseParsingService', () => { page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } }, statusCode: '500' }; - - const definitions = [ + const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 4, totalElements: 4, totalPages: 1, currentPage: 1 }); + const definitions = Object.assign(new SubmissionDefinitionsModel(), { isDefault: true, name: 'traditional', type: 'submissiondefinition', - _links: {}, - sections: [ - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.describe.stepone', - mandatory: true, - sectionType: 'submission-form', - visibility:{ - main:null, - other:'READONLY' - }, - type: 'submissionsection', - _links: {} - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.describe.steptwo', - mandatory: true, - sectionType: 'submission-form', - visibility:{ - main:null, - other:'READONLY' - }, - type: 'submissionsection', - _links: {} - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.upload', - mandatory: false, - sectionType: 'upload', - visibility:{ - main:null, - other:'READONLY' - }, - type: 'submissionsection', - _links: {} - }), - Object.assign(new SubmissionSectionModel(), { - header: 'submit.progressbar.license', - mandatory: true, - sectionType: 'license', - visibility:{ - main:null, - other:'READONLY' - }, - type: 'submissionsection', - _links: {} - }) - ] - }) - ]; + _links: { + sections: 'https://rest.api/config/submissiondefinitions/traditional/sections', + self: 'https://rest.api/config/submissiondefinitions/traditional' + }, + self: 'https://rest.api/config/submissiondefinitions/traditional', + sections: new PaginatedList(pageinfo, [ + 'https://rest.api/config/submissionsections/traditionalpageone', + 'https://rest.api/config/submissionsections/traditionalpagetwo', + 'https://rest.api/config/submissionsections/upload', + 'https://rest.api/config/submissionsections/license' + ]) + }); it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { const response = service.parse(validRequest, validResponse); diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index dfbbfc50c769cda17eeafd1323534fa6b6db255c..2b1b9236256e86211a0e21292b0b25425c79c725 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -29,7 +29,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) { const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href); - return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload)); + return new ConfigSuccessResponse(configDefinition, data.statusCode, this.processPageInfo(data.payload)); } else { return new ErrorResponse( Object.assign( diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8377afe92e4543aeeff1249ee86fe1b95cbb7365 --- /dev/null +++ b/src/app/core/data/data.service.spec.ts @@ -0,0 +1,133 @@ +import { DataService } from './data.service'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { Store } from '@ngrx/store'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Observable } from 'rxjs/Observable'; +import { FindAllOptions } from './request.models'; +import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; + +const LINK_NAME = 'test' + +// tslint:disable:max-classes-per-file +class NormalizedTestObject extends NormalizedObject { +} + +class TestService extends DataService<NormalizedTestObject, any> { + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store<CoreState>, + protected linkPath: string, + protected halService: HALEndpointService + ) { + super(); + } + + public getScopedEndpoint(scope: string): Observable<string> { + throw new Error('getScopedEndpoint is abstract in DataService'); + } + +} + +describe('DataService', () => { + let service: TestService; + let options: FindAllOptions; + const responseCache = {} as ResponseCacheService; + const requestService = {} as RequestService; + const halService = {} as HALEndpointService; + const rdbService = {} as RemoteDataBuildService; + const store = {} as Store<CoreState>; + const endpoint = 'https://rest.api/core'; + + function initTestService(): TestService { + return new TestService( + responseCache, + requestService, + rdbService, + store, + LINK_NAME, + halService + ); + } + + service = initTestService(); + + describe('getFindAllHref', () => { + + it('should return an observable with the endpoint', () => { + options = {}; + + (service as any).getFindAllHref(endpoint).subscribe((value) => { + expect(value).toBe(endpoint); + } + ); + }); + + // getScopedEndpoint is not implemented in abstract DataService + it('should throw error if scopeID provided in options', () => { + options = { scopeID: 'somevalue' }; + + expect(() => { (service as any).getFindAllHref(endpoint, options) }) + .toThrowError('getScopedEndpoint is abstract in DataService'); + }); + + it('should include page in href if currentPage provided in options', () => { + options = { currentPage: 2 }; + const expected = `${endpoint}?page=${options.currentPage - 1}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include size in href if elementsPerPage provided in options', () => { + options = { elementsPerPage: 5 }; + const expected = `${endpoint}?size=${options.elementsPerPage}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include sort href if SortOptions provided in options', () => { + const sortOptions = new SortOptions('field1', SortDirection.ASC); + options = { sort: sortOptions}; + const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include startsWith in href if startsWith provided in options', () => { + options = { startsWith: 'ab' }; + const expected = `${endpoint}?startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include all provided options in href', () => { + const sortOptions = new SortOptions('field1', SortDirection.DESC) + options = { + currentPage: 6, + elementsPerPage: 10, + sort: sortOptions, + startsWith: 'ab' + } + const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }) + }); + +}); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 5c01412ba7e8ebf7176141ea18e8a6a5cda35d4d..81381c5f75d6c1bd625b1db340e39a871882634f 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -41,6 +41,10 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain> args.push(`sort=${options.sort.field},${options.sort.direction}`); } + if (hasValue(options.startsWith)) { + args.push(`startsWith=${options.startsWith}`); + } + if (isNotEmpty(args)) { return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()); } else { diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 9651eb3157ca6637fa57deab27a241ceb864316d..aff450781f2079dea115d7c37d36cde10d071ba4 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -12,6 +12,7 @@ import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; import { BaseResponseParsingService } from './base-response-parsing.service'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; @Injectable() export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -27,7 +28,16 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href); - const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self); + let objectList = processRequestDTO; + if (hasNoValue(processRequestDTO)) { + return new DSOSuccessResponse([], data.statusCode, undefined) + } + if (hasValue(processRequestDTO.page)) { + objectList = processRequestDTO.page; + } else if (!Array.isArray(processRequestDTO)) { + objectList = [processRequestDTO]; + } + const selfLinks = objectList.map((no) => no.self); return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload)) } diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index e2e2f92d5aa6c71e1a9c59b3c506c22b3960cbd3..9c3e5b0344150d58f936dc883c1833dd3fc3907b 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -8,6 +8,8 @@ import { CoreState } from '../core.reducers'; import { IntegrationResponseParsingService } from './integration-response-parsing.service'; import { IntegrationRequest } from '../data/request.models'; import { AuthorityValueModel } from './models/authority-value.model'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedList } from '../data/paginated-list'; describe('IntegrationResponseParsingService', () => { let service: IntegrationResponseParsingService; @@ -78,7 +80,7 @@ describe('IntegrationResponseParsingService', () => { }, _links: { - self: 'https://rest.api/integration/authorities/type/entries' + self: { href: 'https://rest.api/integration/authorities/type/entries' } } }, statusCode: '200' @@ -141,39 +143,44 @@ describe('IntegrationResponseParsingService', () => { }, statusCode: '200' }; - - const definitions = [ - Object.assign(new AuthorityValueModel(), { + const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1 }); + const definitions = new PaginatedList(pageinfo,[ + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', display: 'One', id: 'One', - otherInformation: {}, + otherInformation: undefined, value: 'One' }), - Object.assign(new AuthorityValueModel(), { + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', display: 'Two', id: 'Two', - otherInformation: {}, + otherInformation: undefined, value: 'Two' }), - Object.assign(new AuthorityValueModel(), { + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', display: 'Three', id: 'Three', - otherInformation: {}, + otherInformation: undefined, value: 'Three' }), - Object.assign(new AuthorityValueModel(), { + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', display: 'Four', id: 'Four', - otherInformation: {}, + otherInformation: undefined, value: 'Four' }), - Object.assign(new AuthorityValueModel(), { + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', display: 'Five', id: 'Five', - otherInformation: {}, + otherInformation: undefined, value: 'Five' }) - ]; + ]); it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => { const response = service.parse(validRequest, validResponse); diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts index 5d6ce09114a2db7dac026de8ec297fad062a417f..06c6b9620d06706e50ab26e0ac5003fe2d1b7b93 100644 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -33,7 +33,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { const dataDefinition = this.process<IntegrationModel,IntegrationType>(data.payload, request.href); - return new IntegrationSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page)); + return new IntegrationSuccessResponse(dataDefinition, data.statusCode, this.processPageInfo(data.payload.page)); } else { return new ErrorResponse( Object.assign( diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index b85d9e2053dac7fd1c4387a4219d613bb7cc8534..9af345e6074f6f64c48cdb9b520042aaf2868ad5 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -1,24 +1,55 @@ -import { autoserialize } from 'cerialize'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { ResourceType } from './resource-type'; -export class BitstreamFormat { +/** + * Model class for a Bitstream Format + */ +export class BitstreamFormat implements CacheableObject { - @autoserialize + /** + * Short description of this Bitstream Format + */ shortDescription: string; - @autoserialize + /** + * Description of this Bitstream Format + */ description: string; - @autoserialize + /** + * String representing the MIME type of this Bitstream Format + */ mimetype: string; - @autoserialize + /** + * The level of support the system offers for this Bitstream Format + */ supportLevel: number; - @autoserialize + /** + * True if the Bitstream Format is used to store system information, rather than the content of items in the system + */ internal: boolean; - @autoserialize + /** + * String representing this Bitstream Format's file extension + */ extensions: string; + /** + * The link to the rest endpoint where this Bitstream Format can be found + */ + self: string; + + /** + * A ResourceType representing the kind of Object of this BitstreamFormat + */ + type: ResourceType; + + /** + * Universally unique identifier for this Bitstream Format + */ + uuid: string; + } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index c34666b0f06d50eac1340f0563ae9c07e3bc19fa..8fd55d312f055508c790779e2a53e8addb32c26d 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -3,6 +3,7 @@ import { Bitstream } from './bitstream.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs/Observable'; +import { PaginatedList } from '../data/paginated-list'; export class Community extends DSpaceObject { @@ -58,6 +59,6 @@ export class Community extends DSpaceObject { */ owner: Observable<RemoteData<Community>>; - collections: Observable<RemoteData<Collection[]>>; + collections: Observable<RemoteData<PaginatedList<Collection>>>; } diff --git a/src/app/core/shared/config/config-submission-definitions.model.ts b/src/app/core/shared/config/config-submission-definitions.model.ts index 8249d2b118af5a72569134ce16365f70406cdf9c..0247f13944c8bbe9a69a00bbb4aa220f56b03889 100644 --- a/src/app/core/shared/config/config-submission-definitions.model.ts +++ b/src/app/core/shared/config/config-submission-definitions.model.ts @@ -1,6 +1,7 @@ import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { ConfigObject } from './config.model'; import { SubmissionSectionModel } from './config-submission-section.model'; +import { PaginatedList } from '../../data/paginated-list'; @inheritSerialization(ConfigObject) export class SubmissionDefinitionsModel extends ConfigObject { @@ -9,6 +10,6 @@ export class SubmissionDefinitionsModel extends ConfigObject { isDefault: boolean; @autoserializeAs(SubmissionSectionModel) - sections: SubmissionSectionModel[]; + sections: PaginatedList<SubmissionSectionModel>; } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index dd60ad9b01ce9776a8d6550ee8109a2440454eae..cc84694e84707126c404c02c7079fbf15ce9515a 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -5,6 +5,7 @@ import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { PaginatedList } from '../data/paginated-list'; export class Item extends DSpaceObject { @@ -47,7 +48,7 @@ export class Item extends DSpaceObject { return this.owningCollection; } - bitstreams: Observable<RemoteData<Bitstream[]>>; + bitstreams: Observable<RemoteData<PaginatedList<Bitstream>>>; /** * Retrieves the thumbnail of this item @@ -88,7 +89,7 @@ export class Item extends DSpaceObject { */ getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> { return this.bitstreams - .map((rd: RemoteData<Bitstream[]>) => rd.payload) + .map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page) .filter((bitstreams: Bitstream[]) => hasValue(bitstreams)) .startWith([]) .map((bitstreams) => { diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..cccbea1e892d3e5c264d7cfaa0d79ee2e3104af6 --- /dev/null +++ b/src/app/core/shared/resource-policy.model.ts @@ -0,0 +1,40 @@ +import { CacheableObject } from '../cache/object-cache.reducer'; +import { ResourceType } from './resource-type'; +import { Group } from '../eperson/models/group.model'; +import { ActionType } from '../cache/models/action-type.model'; + +/** + * Model class for a Resource Policy + */ +export class ResourcePolicy implements CacheableObject { + /** + * The action that is allowed by this Resource Policy + */ + action: ActionType; + + /** + * The name for this Resource Policy + */ + name: string; + + /** + * The Group this Resource Policy applies to + */ + group: Group; + + /** + * The link to the rest endpoint where this Resource Policy can be found + */ + self: string; + + /** + * A ResourceType representing the kind of Object of this ResourcePolicy + */ + type: ResourceType; + + /** + * The universally unique identifier for this Resource Policy + */ + uuid: string; + +} diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index b774188f630538e69af8e75cf9fa0795e627787f..71053f628b420960e5d5d2c89cfc94c0af24756e 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -8,4 +8,5 @@ export enum ResourceType { Community = 'community', Eperson = 'eperson', Group = 'group', + ResourcePolicy = 'resourcePolicy' } diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts index 81c1fcf26cf93ee8a746b437098b920a22ac2d45..f6dd0be861d996385df1f9eb97c1049eee104e1d 100644 --- a/src/app/shared/mocks/mock-item.ts +++ b/src/app/shared/mocks/mock-item.ts @@ -17,78 +17,86 @@ export const MockItem: Item = Object.assign(new Item(), { errorMessage: '', statusCode: '202', pageInfo: {}, - payload: [ - { - sizeBytes: 10201, - content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', - format: Observable.of({ - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10', - requestPending: false, - responsePending: false, - isSuccessful: true, - errorMessage: '', - statusCode: '202', - pageInfo: {}, - payload: { - shortDescription: 'Microsoft Word XML', - description: 'Microsoft Word XML', - mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - supportLevel: 0, - internal: false, - extensions: null, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10' - } - }), - bundleName: 'ORIGINAL', - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713', - id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', - uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', - type: 'bitstream', - name: 'test_word.docx', - metadata: [ - { - key: 'dc.title', - language: null, - value: 'test_word.docx' - } - ] + payload: { + pageInfo: { + elementsPerPage: 20, + totalElements: 3, + totalPages: 1, + currentPage: 2 }, - { - sizeBytes: 31302, - content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content', - format: Observable.of({ - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4', - requestPending: false, - responsePending: false, - isSuccessful: true, - errorMessage: '', - statusCode: '202', - pageInfo: {}, - payload: { - shortDescription: 'Adobe PDF', - description: 'Adobe Portable Document Format', - mimetype: 'application/pdf', - supportLevel: 0, - internal: false, - extensions: null, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' - } - }), - bundleName: 'ORIGINAL', - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28', - id: '99b00f3c-1cc6-4689-8158-91965bee6b28', - uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', - type: 'bitstream', - name: 'test_pdf.pdf', - metadata: [ - { - key: 'dc.title', - language: null, - value: 'test_pdf.pdf' - } - ] - } - ] + page: [ + { + sizeBytes: 10201, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', + format: Observable.of({ + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10', + requestPending: false, + responsePending: false, + isSuccessful: true, + errorMessage: '', + statusCode: '202', + pageInfo: {}, + payload: { + shortDescription: 'Microsoft Word XML', + description: 'Microsoft Word XML', + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + supportLevel: 0, + internal: false, + extensions: null, + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10' + } + }), + bundleName: 'ORIGINAL', + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713', + id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + type: 'bitstream', + name: 'test_word.docx', + metadata: [ + { + key: 'dc.title', + language: null, + value: 'test_word.docx' + } + ] + }, + { + sizeBytes: 31302, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content', + format: Observable.of({ + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4', + requestPending: false, + responsePending: false, + isSuccessful: true, + errorMessage: '', + statusCode: '202', + pageInfo: {}, + payload: { + shortDescription: 'Adobe PDF', + description: 'Adobe Portable Document Format', + mimetype: 'application/pdf', + supportLevel: 0, + internal: false, + extensions: null, + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' + } + }), + bundleName: 'ORIGINAL', + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28', + id: '99b00f3c-1cc6-4689-8158-91965bee6b28', + uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', + type: 'bitstream', + name: 'test_pdf.pdf', + metadata: [ + { + key: 'dc.title', + language: null, + value: 'test_pdf.pdf' + } + ] + } + ] + } }), self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', diff --git a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts index d52036b5dc5a648574c6d3f8edbbc43cdec96535..59df86fdffd4ad90ec224aac0dc99c1f5cccd924 100644 --- a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts +++ b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts @@ -1,5 +1,6 @@ import { Component, Inject } from '@angular/core'; import { ListableObject } from '../listable-object.model'; +import { hasValue } from '../../../empty.util'; @Component({ selector: 'ds-abstract-object-element', @@ -10,4 +11,8 @@ export class AbstractListableElementComponent <T extends ListableObject> { public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) { this.object = listableObject as T; } + + hasValue(data) { + return hasValue(data); + } } diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html index ee1b9c42399e301bc9173e9e33602d2af0e4623e..728dba754968a2b678f65df86cc27ec59e58783b 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html @@ -12,7 +12,7 @@ <span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}} <span *ngIf="!last">; </span> </span> - <span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span> + <span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span> </p> </ds-truncatable-part> diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index 1cf14587ad4d442a7e28c97691e4c6c5d7c38e86..1e4f6f3c647f47abd0dc6c1abf0dd6ed751adf7c 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -13,7 +13,7 @@ <p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted"> <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1"> - <span *ngIf="dso.findMetadata('dc.date.issued').length > 0" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span> + <span *ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span> <span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">, <span [innerHTML]="authorMd.value"></span> </span> diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index e6217eb0bbf470fb296901efd57f36e1859e2061..5ab9f472b40081223b189afd1dcc17aea9f52824 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -3,7 +3,7 @@ import { Component, Inject } from '@angular/core'; import { SearchResult } from '../../../+search-page/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { Metadatum } from '../../../core/shared/metadatum.model'; -import { isEmpty, hasNoValue } from '../../empty.util'; +import { isEmpty, hasNoValue, hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { TruncatableService } from '../../truncatable/truncatable.service'; @@ -60,4 +60,5 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten isCollapsed(): Observable<boolean> { return this.truncatableService.isCollapsed(this.dso.id); } + } diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.html b/src/app/shared/object-list/item-list-element/item-list-element.component.html index 28b83b40009a4caf455b8545ea000e8eb0ecb1b8..711ce1903755f73ba0c72d46545ecdb517af0e9d 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.html @@ -11,8 +11,8 @@ <span *ngIf="!last">; </span> </span> </span> - (<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span - *ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>) + (<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span + *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>) </span> </ds-truncatable-part> <ds-truncatable-part [id]="object.id" [minLines]="3"> diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index b8f3197a7c719d172fb9aaa24f153758af4a54b2..e1f559dd66cfd6a769fc2d2b60859b50d10f7461 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -6,7 +6,7 @@ <ds-truncatable-part [id]="dso.id" [minLines]="1"> (<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher" [innerHTML]="getFirstValue('dc.publisher')">, </span><span - *ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date" + *ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-list-date" [innerHTML]="getFirstValue('dc.date.issued')"></span>) <span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-list-authors">