diff --git a/resources/i18n/en.json b/resources/i18n/en.json index e3126b16b8..667d5132ba 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -207,7 +207,7 @@ "f.dateIssued.max": "End date", "f.subject": "Subject", "f.has_content_in_original_bundle": "Has files", - "f.entityType": "Item Type" + "f.entityType": "Entity Type" }, "filter": { "show-more": "Show more", @@ -237,8 +237,8 @@ "head": "Has files" }, "entityType": { - "placeholder": "Item Type", - "head": "Item Type" + "placeholder": "Entity Type", + "head": "Entity Type" } } } diff --git a/src/app/+item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/+item-page/field-components/metadata-values/metadata-values.component.spec.ts index 9447e1c48e..9682386b96 100644 --- a/src/app/+item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/+item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -4,6 +4,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader'; import { MetadataValuesComponent } from './metadata-values.component'; import { By } from '@angular/platform-browser'; +import { Metadatum } from '../../../core/shared/metadatum.model'; let comp: MetadataValuesComponent; let fixture: ComponentFixture; @@ -23,7 +24,7 @@ const mockMetadata = [ key: 'journal.identifier.description', language: 'en_US', value: 'desc' - }]; + }] as Metadatum[]; const mockSeperator = '
'; const mockLabel = 'fake.message'; diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index b4e5ba1cd8..41d78f6393 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -29,6 +29,7 @@ import { JournalComponent } from './simple/item-types/journal/journal.component' import { JournalVolumeComponent } from './simple/item-types/journal-volume/journal-volume.component'; import { JournalIssueComponent } from './simple/item-types/journal-issue/journal-issue.component'; import { ItemComponent } from './simple/item-types/shared/item.component'; +import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component'; @NgModule({ imports: [ @@ -61,7 +62,8 @@ import { ItemComponent } from './simple/item-types/shared/item.component'; GenericItemPageFieldComponent, JournalComponent, JournalIssueComponent, - JournalVolumeComponent + JournalVolumeComponent, + MetadataRepresentationListComponent ], entryComponents: [ PublicationComponent, diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index e59d37fb9d..b6de496dc4 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,7 +1,7 @@
- +
diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 1cee863070..ac23add738 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -15,6 +15,8 @@ import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; import * as viewMode from '../../shared/view-mode'; +export const VIEW_MODE_FULL = 'full'; + /** * This component renders a simple item page. * The route parameter 'id' is used to request the item it represents. @@ -46,9 +48,8 @@ export class ItemPageComponent implements OnInit { /** * The view-mode we're currently on - * @type {ElementViewMode} */ - ElementViewMode = viewMode.ElementViewMode; + viewMode = VIEW_MODE_FULL; constructor( private route: ActivatedRoute, diff --git a/src/app/+item-page/simple/item-types/journal-issue/journal-issue.component.ts b/src/app/+item-page/simple/item-types/journal-issue/journal-issue.component.ts index c663b15256..9240616c59 100644 --- a/src/app/+item-page/simple/item-types/journal-issue/journal-issue.component.ts +++ b/src/app/+item-page/simple/item-types/journal-issue/journal-issue.component.ts @@ -4,11 +4,11 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('JournalIssue', ElementViewMode.Full) +@rendersItemType('JournalIssue', VIEW_MODE_FULL) @Component({ selector: 'ds-journal-issue', styleUrls: ['./journal-issue.component.scss'], diff --git a/src/app/+item-page/simple/item-types/journal-volume/journal-volume.component.ts b/src/app/+item-page/simple/item-types/journal-volume/journal-volume.component.ts index ebb6919234..0372fe5d30 100644 --- a/src/app/+item-page/simple/item-types/journal-volume/journal-volume.component.ts +++ b/src/app/+item-page/simple/item-types/journal-volume/journal-volume.component.ts @@ -4,11 +4,11 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('JournalVolume', ElementViewMode.Full) +@rendersItemType('JournalVolume', VIEW_MODE_FULL) @Component({ selector: 'ds-journal-volume', styleUrls: ['./journal-volume.component.scss'], diff --git a/src/app/+item-page/simple/item-types/journal/journal.component.ts b/src/app/+item-page/simple/item-types/journal/journal.component.ts index 3af725062c..12ba91eb6b 100644 --- a/src/app/+item-page/simple/item-types/journal/journal.component.ts +++ b/src/app/+item-page/simple/item-types/journal/journal.component.ts @@ -4,11 +4,11 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('Journal', ElementViewMode.Full) +@rendersItemType('Journal', VIEW_MODE_FULL) @Component({ selector: 'ds-journal', styleUrls: ['./journal.component.scss'], diff --git a/src/app/+item-page/simple/item-types/orgunit/orgunit.component.ts b/src/app/+item-page/simple/item-types/orgunit/orgunit.component.ts index f1979b0961..1a6ab4ba36 100644 --- a/src/app/+item-page/simple/item-types/orgunit/orgunit.component.ts +++ b/src/app/+item-page/simple/item-types/orgunit/orgunit.component.ts @@ -3,12 +3,12 @@ import { Observable } from 'rxjs'; import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('OrgUnit', ElementViewMode.Full) +@rendersItemType('OrgUnit', VIEW_MODE_FULL) @Component({ selector: 'ds-orgunit', styleUrls: ['./orgunit.component.scss'], diff --git a/src/app/+item-page/simple/item-types/person/person.component.ts b/src/app/+item-page/simple/item-types/person/person.component.ts index bb98799da5..3cf5a230bf 100644 --- a/src/app/+item-page/simple/item-types/person/person.component.ts +++ b/src/app/+item-page/simple/item-types/person/person.component.ts @@ -4,12 +4,12 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('Person', ElementViewMode.Full) +@rendersItemType('Person', VIEW_MODE_FULL) @Component({ selector: 'ds-person', styleUrls: ['./person.component.scss'], diff --git a/src/app/+item-page/simple/item-types/project/project.component.ts b/src/app/+item-page/simple/item-types/project/project.component.ts index dfbdacff86..4bdb6012f2 100644 --- a/src/app/+item-page/simple/item-types/project/project.component.ts +++ b/src/app/+item-page/simple/item-types/project/project.component.ts @@ -3,12 +3,12 @@ import { Observable } from 'rxjs'; import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersItemType } from '../../../../shared/items/item-type-decorator'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('Project', ElementViewMode.Full) +@rendersItemType('Project', VIEW_MODE_FULL) @Component({ selector: 'ds-project', styleUrls: ['./project.component.scss'], diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index e619e18b01..b1259be689 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -9,10 +9,10 @@
- - + + diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.ts b/src/app/+item-page/simple/item-types/publication/publication.component.ts index b6178893d6..1ea50598c7 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/+item-page/simple/item-types/publication/publication.component.ts @@ -7,11 +7,12 @@ import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; +import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { VIEW_MODE_FULL } from '../../item-page.component'; -@rendersItemType('Publication', ElementViewMode.Full) -@rendersItemType(DEFAULT_ITEM_TYPE, ElementViewMode.Full) +@rendersItemType('Publication', VIEW_MODE_FULL) +@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_FULL) @Component({ selector: 'ds-publication', styleUrls: ['./publication.component.scss'], @@ -22,7 +23,7 @@ export class PublicationComponent extends ItemComponent implements OnInit { /** * The authors related to this publication */ - authors$: Observable; + authors$: Observable; /** * The projects related to this publication @@ -51,10 +52,7 @@ export class PublicationComponent extends ItemComponent implements OnInit { if (this.resolvedRelsAndTypes$) { - this.authors$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isAuthorOfPublication'), - relationsToItems(this.item.id, this.ids) - ); + this.authors$ = this.buildRepresentations('Person', 'dc.contributor.author', this.ids); this.projects$ = this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel('isProjectOfPublication'), diff --git a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts index 30f39957db..8c6cf15ee9 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts @@ -16,8 +16,18 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; -import { compareArraysUsing, compareArraysUsingIds } from './item.component'; +import { compareArraysUsing, compareArraysUsingIds, ItemComponent } from './item.component'; import { of as observableOf } from 'rxjs'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ItemPageComponent } from '../../item-page.component'; +import { VarDirective } from '../../../../shared/utils/var.directive'; +import { ActivatedRoute } from '@angular/router'; +import { MetadataService } from '../../../../core/metadata/metadata.service'; +import { of } from 'rxjs/internal/observable/of'; +import { Observable } from 'rxjs/internal/Observable'; +import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-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'; /** * Create a generic test for an item-page-fields component using a mockItem and the type of component @@ -306,4 +316,116 @@ describe('ItemComponent', () => { }); }); + describe('when calling buildRepresentations', () => { + let comp: ItemComponent; + let fixture: ComponentFixture; + + const metadataField = 'dc.contributor.author'; + const mockItem = Object.assign(new Item(), { + id: '1', + uuid: '1', + metadata: [ + { + key: metadataField, + value: 'Second value', + place: 1 + }, + { + key: metadataField, + value: 'Third value', + place: 2, + authority: 'virtual::123' + }, + { + key: metadataField, + value: 'First value', + place: 0 + }, + { + key: metadataField, + value: 'Fourth value', + place: 3, + authority: '123' + } + ], + relationships: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [ + Object.assign(new Relationship(), { + uuid: '123', + id: '123', + leftId: '1', + rightId: '2', + relationshipType: observableOf(new RemoteData(false, false, true, null, new RelationshipType())) + }) + ]))) + }); + const relatedItem = Object.assign(new Item(), { + id: '2', + metadata: [ + { + key: 'dc.title', + value: 'related item' + } + ] + }); + const mockItemDataService = { + findById: (id) => { + if (id === relatedItem.id) { + return observableOf(new RemoteData(false, false, true, null, relatedItem)) + } + } + } as ItemDataService; + + let representations: Observable; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), BrowserAnimationsModule], + declarations: [ItemComponent, VarDirective], + providers: [ + {provide: ITEM, useValue: mockItem} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(ItemComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + representations = comp.buildRepresentations('bogus', metadataField, mockItemDataService); + })); + + it('should contain exactly 4 metadata-representations', () => { + representations.subscribe((reps: MetadataRepresentation[]) => { + expect(reps.length).toEqual(4); + }); + }); + + it('should have all the representations in the correct order', () => { + representations.subscribe((reps: MetadataRepresentation[]) => { + expect(reps[0].getPrimaryValue()).toEqual('First value'); + expect(reps[1].getPrimaryValue()).toEqual('Second value'); + expect(reps[2].getPrimaryValue()).toEqual('related item'); + expect(reps[3].getPrimaryValue()).toEqual('Fourth value'); + }); + }); + + it('should have created the correct MetadatumRepresentation and ItemMetadataRepresentation objects for the correct Metadata', () => { + representations.subscribe((reps: MetadataRepresentation[]) => { + expect(reps[0] instanceof MetadatumRepresentation).toEqual(true); + expect(reps[1] instanceof MetadatumRepresentation).toEqual(true); + expect(reps[2] instanceof ItemMetadataRepresentation).toEqual(true); + expect(reps[3] instanceof MetadatumRepresentation).toEqual(true); + }); + }); + }) + }); diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index 5527114ec3..c5aae65f13 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -7,9 +7,14 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { getRemoteDataPayload } from '../../../../core/shared/operators'; -import { hasValue } from '../../../../shared/empty.util'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { hasNoValue, hasValue } from '../../../../shared/empty.util'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; +import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { Metadatum } from '../../../../core/shared/metadatum.model'; +import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { of } from 'rxjs/internal/observable/of'; /** * Operator for comparing arrays using a mapping function @@ -82,6 +87,41 @@ export const relationsToItems = (thisId: string, ids: ItemDataService) => distinctUntilChanged(compareArraysUsingIds()), ); +/** + * Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata + * @param thisId The id of the parent item + * @param itemType The type of relation this list resembles (for creating representations) + * @param metadata The list of original Metadatum objects + * @param ids The ItemDataService to use for fetching Items from the Rest API + */ +export const relationsToRepresentations = (thisId: string, itemType: string, metadata: Metadatum[], ids: ItemDataService) => + (source: Observable): Observable => + source.pipe( + flatMap((rels: Relationship[]) => + observableZip( + ...metadata.map((metadatum: Metadatum) => { + const prefix = 'virtual::'; + if (hasValue(metadatum.authority) && metadatum.authority.startsWith(prefix)) { + const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.authority.substring(metadatum.authority.indexOf(prefix) + prefix.length)); + if (matchingRels.length > 0) { + const matchingRel = matchingRels[0]; + let queryId = matchingRel.leftId; + if (matchingRel.leftId === thisId) { + queryId = matchingRel.rightId; + } + return ids.findById(queryId).pipe( + getSucceededRemoteData(), + map((d: RemoteData) => Object.assign(new ItemMetadataRepresentation(itemType), d.payload)) + ); + } + } else { + return of(Object.assign(new MetadatumRepresentation(itemType), metadatum)); + } + }) + ) + ) + ); + @Component({ selector: 'ds-item', template: '' @@ -93,7 +133,7 @@ export class ItemComponent implements OnInit { /** * Resolved relationships and types together in one observable */ - resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]> + resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>; constructor( @Inject(ITEM) public item: Item @@ -125,4 +165,25 @@ export class ItemComponent implements OnInit { } } + /** + * Build a list of MetadataRepresentations for the current item. This combines all metadata and relationships of a + * certain type. + * @param itemType The type of item we're building representations of. Used for matching templates. + * @param metadataField The metadata field that resembles the item type. + * @param itemDataService ItemDataService to turn relations into items. + */ + buildRepresentations(itemType: string, metadataField: string, itemDataService: ItemDataService): Observable { + const metadata = this.item.findMetadataSortedByPlace(metadataField); + const relsCurrentPage$ = this.item.relationships.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((pl: PaginatedList) => pl.page), + distinctUntilChanged(compareArraysUsingIds()) + ); + + return relsCurrentPage$.pipe( + relationsToRepresentations(this.item.id, itemType, metadata, itemDataService) + ); + } + } diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html new file mode 100644 index 0000000000..48eabf8451 --- /dev/null +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html @@ -0,0 +1,5 @@ + + + + diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts new file mode 100644 index 0000000000..77ba30778e --- /dev/null +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts @@ -0,0 +1,40 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { MetadataRepresentationListComponent } from './metadata-representation-list.component'; +import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; + +const itemType = 'type'; +const metadataRepresentation1 = new MetadatumRepresentation(itemType); +const metadataRepresentation2 = new ItemMetadataRepresentation(itemType); +const representations = [metadataRepresentation1, metadataRepresentation2]; + +describe('MetadataRepresentationListComponent', () => { + let comp: MetadataRepresentationListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [MetadataRepresentationListComponent], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(MetadataRepresentationListComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(MetadataRepresentationListComponent); + comp = fixture.componentInstance; + comp.representations = representations; + fixture.detectChanges(); + })); + + it(`should load ${representations.length} item-type-switcher components`, () => { + const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher')); + expect(fields.length).toBe(representations.length); + }); + +}); diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts new file mode 100644 index 0000000000..b821557f4a --- /dev/null +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core'; +import * as viewMode from '../../../shared/view-mode'; +import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; + +export const VIEW_MODE_METADATA = 'metadata'; + +@Component({ + selector: 'ds-metadata-representation-list', + templateUrl: './metadata-representation-list.component.html' +}) +/** + * This component is used for displaying metadata + * It expects a list of MetadataRepresentation objects and a label to put on top of the list + */ +export class MetadataRepresentationListComponent { + /** + * A list of metadata-representations to display + */ + @Input() representations: MetadataRepresentation[]; + + /** + * An i18n label to use as a title for the list + */ + @Input() label: string; + + /** + * The view-mode we're currently on + * @type {ElementViewMode} + */ + viewMode = VIEW_MODE_METADATA; +} diff --git a/src/app/+item-page/simple/related-items/related-items-component.ts b/src/app/+item-page/simple/related-items/related-items-component.ts index 6ad2f0eeb3..ce8ca58b29 100644 --- a/src/app/+item-page/simple/related-items/related-items-component.ts +++ b/src/app/+item-page/simple/related-items/related-items-component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import * as viewMode from '../../../shared/view-mode'; + +export const VIEW_MODE_ELEMENT = 'element'; @Component({ selector: 'ds-related-items', @@ -26,5 +27,5 @@ export class RelatedItemsComponent { * The view-mode we're currently on * @type {ElementViewMode} */ - ElementViewMode = viewMode.ElementViewMode + viewMode = VIEW_MODE_ELEMENT; } diff --git a/src/app/+item-page/simple/related-items/related-items.component.html b/src/app/+item-page/simple/related-items/related-items.component.html index bd8b7eb5f3..4b284ad63c 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.html +++ b/src/app/+item-page/simple/related-items/related-items.component.html @@ -1,5 +1,5 @@ + [object]="item" [viewMode]="viewMode"> diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 006707d710..189517e9f5 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -33,6 +33,7 @@ import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { EmptyError } from 'rxjs/internal-compatibility'; +import { Metadatum } from '../shared/metadatum.model'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -223,7 +224,7 @@ describe('MetadataService', () => { key: 'dc.publisher', language: 'en_US', value: 'Mock Publisher' - }); + } as Metadatum); return publishedMockItem; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 3a40d142aa..916274eda6 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,5 +1,5 @@ import { Metadatum } from './metadatum.model' -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; @@ -91,4 +91,23 @@ export class DSpaceObject implements CacheableObject, ListableObject { }); } + /** + * Find metadata on a specific field and order all of them using their "place" property. + * @param key + */ + findMetadataSortedByPlace(key: string): Metadatum[] { + return this.filterMetadata([key]).sort((a: Metadatum, b: Metadatum) => { + if (hasNoValue(a.place) && hasNoValue(b.place)) { + return 0; + } + if (hasNoValue(a.place)) { + return -1; + } + if (hasNoValue(b.place)) { + return 1; + } + return a.place - b.place; + }); + } + } diff --git a/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.spec.ts b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.spec.ts new file mode 100644 index 0000000000..4afe5a783f --- /dev/null +++ b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.spec.ts @@ -0,0 +1,36 @@ +import { MetadataRepresentationType } from '../metadata-representation.model'; +import { ItemMetadataRepresentation, ItemTypeToPrimaryValue } from './item-metadata-representation.model'; +import { Item } from '../../item.model'; +import { Metadatum } from '../../metadatum.model'; + +describe('ItemMetadataRepresentation', () => { + const valuePrefix = 'Test value for '; + const item = new Item(); + let itemMetadataRepresentation: ItemMetadataRepresentation; + item.metadata = Object.keys(ItemTypeToPrimaryValue).map((key: string) => { + return Object.assign(new Metadatum(), { + key: ItemTypeToPrimaryValue[key], + value: `${valuePrefix}${ItemTypeToPrimaryValue[key]}` + }); + }); + + for (const itemType of Object.keys(ItemTypeToPrimaryValue)) { + describe(`when creating an ItemMetadataRepresentation with item-type "${itemType}"`, () => { + beforeEach(() => { + itemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(itemType), item); + }); + + it('should have a representation type of item', () => { + expect(itemMetadataRepresentation.representationType).toEqual(MetadataRepresentationType.Item); + }); + + it('should return the correct value when calling getPrimaryValue', () => { + expect(itemMetadataRepresentation.getPrimaryValue()).toEqual(`${valuePrefix}${ItemTypeToPrimaryValue[itemType]}`); + }); + + it('should return the correct item type', () => { + expect(itemMetadataRepresentation.itemType).toEqual(itemType); + }); + }); + } +}); diff --git a/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts new file mode 100644 index 0000000000..9ff72d5cf6 --- /dev/null +++ b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts @@ -0,0 +1,48 @@ +import { Item } from '../../item.model'; +import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model'; +import { hasValue } from '../../../../shared/empty.util'; + +/** + * An object to convert item types into the metadata field it should render for the item's primary value + */ +export const ItemTypeToPrimaryValue = { + Default: 'dc.title', + Person: 'dc.contributor.author' +}; + +/** + * This class defines the way the item it extends should be represented as metadata + */ +export class ItemMetadataRepresentation extends Item implements MetadataRepresentation { + + /** + * The type of item this item can be represented as + */ + itemType: string; + + constructor(itemType: string) { + super(); + this.itemType = itemType; + } + + /** + * Fetch the way this item should be rendered as in a list + */ + get representationType(): MetadataRepresentationType { + return MetadataRepresentationType.Item; + } + + /** + * Get the value to display, depending on the itemType + */ + getPrimaryValue(): string { + let metadata; + if (hasValue(ItemTypeToPrimaryValue[this.itemType])) { + metadata = ItemTypeToPrimaryValue[this.itemType]; + } else { + metadata = ItemTypeToPrimaryValue.Default; + } + return this.findMetadata(metadata); + } + +} diff --git a/src/app/core/shared/metadata-representation/metadata-representation.model.ts b/src/app/core/shared/metadata-representation/metadata-representation.model.ts new file mode 100644 index 0000000000..770c462e8d --- /dev/null +++ b/src/app/core/shared/metadata-representation/metadata-representation.model.ts @@ -0,0 +1,31 @@ +/** + * An Enum defining the representation type of metadata + */ +export enum MetadataRepresentationType { + None = 'none', + Item = 'item', + AuthorityControlled = 'authority_controlled', + PlainText = 'plain_text' +} + +/** + * An interface containing information about how we should represent certain metadata + */ +export interface MetadataRepresentation { + /** + * The type of item this metadata is representing + * e.g. 'Person' + * This can be used for template matching + */ + itemType: string; + + /** + * How we should render the metadata in a list + */ + representationType: MetadataRepresentationType, + + /** + * Fetches the primary value to be displayed + */ + getPrimaryValue(): string +} diff --git a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.spec.ts b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.spec.ts new file mode 100644 index 0000000000..7356a79bbd --- /dev/null +++ b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.spec.ts @@ -0,0 +1,54 @@ +import { Metadatum } from '../../metadatum.model'; +import { MetadatumRepresentation } from './metadatum-representation.model'; +import { MetadataRepresentationType } from '../metadata-representation.model'; + +describe('MetadatumRepresentation', () => { + const itemType = 'Person'; + const normalMetadatum = Object.assign(new Metadatum(), { + key: 'dc.contributor.author', + value: 'Test Author' + }); + const authorityMetadatum = Object.assign(new Metadatum(), { + key: 'dc.contributor.author', + value: 'Test Authority Author', + authority: '1234' + }); + + let metadatumRepresentation: MetadatumRepresentation; + + describe('when creating a MetadatumRepresentation based on a standard Metadatum object', () => { + beforeEach(() => { + metadatumRepresentation = Object.assign(new MetadatumRepresentation(itemType), normalMetadatum); + }); + + it('should have a representation type of plain text', () => { + expect(metadatumRepresentation.representationType).toEqual(MetadataRepresentationType.PlainText); + }); + + it('should return the correct value when calling getPrimaryValue', () => { + expect(metadatumRepresentation.getPrimaryValue()).toEqual(normalMetadatum.value); + }); + + it('should return the correct item type', () => { + expect(metadatumRepresentation.itemType).toEqual(itemType); + }); + }); + + describe('when creating a MetadatumRepresentation based on an authority controlled Metadatum object', () => { + beforeEach(() => { + metadatumRepresentation = Object.assign(new MetadatumRepresentation(itemType), authorityMetadatum); + }); + + it('should have a representation type of plain text', () => { + expect(metadatumRepresentation.representationType).toEqual(MetadataRepresentationType.AuthorityControlled); + }); + + it('should return the correct value when calling getPrimaryValue', () => { + expect(metadatumRepresentation.getPrimaryValue()).toEqual(authorityMetadatum.value); + }); + + it('should return the correct item type', () => { + expect(metadatumRepresentation.itemType).toEqual(itemType); + }); + }); +}); diff --git a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts new file mode 100644 index 0000000000..6a8de97733 --- /dev/null +++ b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts @@ -0,0 +1,38 @@ +import { Metadatum } from '../../metadatum.model'; +import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model'; +import { hasValue } from '../../../../shared/empty.util'; + +/** + * This class defines the way the metadatum it extends should be represented + */ +export class MetadatumRepresentation extends Metadatum implements MetadataRepresentation { + + /** + * The type of item this metadatum can be represented as + */ + itemType: string; + + constructor(itemType: string) { + super(); + this.itemType = itemType; + } + + /** + * Fetch the way this metadatum should be rendered as in a list + */ + get representationType(): MetadataRepresentationType { + if (hasValue(this.authority)) { + return MetadataRepresentationType.AuthorityControlled; + } else { + return MetadataRepresentationType.PlainText; + } + } + + /** + * Get the value to display + */ + getPrimaryValue(): string { + return this.value; + } + +} diff --git a/src/app/core/shared/metadatum.model.ts b/src/app/core/shared/metadatum.model.ts index a3c5830608..c4ce8c3101 100644 --- a/src/app/core/shared/metadatum.model.ts +++ b/src/app/core/shared/metadatum.model.ts @@ -20,4 +20,17 @@ export class Metadatum { @autoserialize value: string; + /** + * The place of this Metadatum within his list of metadata + * This is used to render metadata in a specific custom order + */ + @autoserialize + place: number; + + /** + * The authority key used for authority-controlled metadata + */ + @autoserialize + authority: string; + } diff --git a/src/app/shared/items/item-type-decorator.ts b/src/app/shared/items/item-type-decorator.ts index 509a1c754a..c8a1a2df97 100644 --- a/src/app/shared/items/item-type-decorator.ts +++ b/src/app/shared/items/item-type-decorator.ts @@ -1,36 +1,60 @@ import { hasNoValue, hasValue } from '../empty.util'; -import { ElementViewMode } from '../view-mode'; +import { MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model'; +import { VIEW_MODE_ELEMENT } from '../../+item-page/simple/related-items/related-items-component'; export const DEFAULT_ITEM_TYPE = 'Default'; +export const DEFAULT_VIEW_MODE = VIEW_MODE_ELEMENT; +export const NO_REPRESENTATION_TYPE = MetadataRepresentationType.None; +export const DEFAULT_REPRESENTATION_TYPE = MetadataRepresentationType.PlainText; const map = new Map(); /** - * Decorator used for rendering simple item pages by type and viewMode + * Decorator used for rendering simple item pages by type and viewMode (and optionally a representationType) * @param type * @param viewMode + * @param representationType */ -export function rendersItemType(type: string, viewMode: ElementViewMode) { +export function rendersItemType(type: string, viewMode: string, representationType?: MetadataRepresentationType) { return function decorator(component: any) { if (hasNoValue(map.get(viewMode))) { map.set(viewMode, new Map()); } - if (hasValue(map.get(viewMode).get(type))) { - throw new Error(`There can't be more than one component to render Items of type "${type}" in view mode "${viewMode}"`); + if (hasNoValue(map.get(viewMode).get(type))) { + map.get(viewMode).set(type, new Map()); } - map.get(viewMode).set(type, component); + if (hasNoValue(representationType)) { + representationType = NO_REPRESENTATION_TYPE; + } + if (hasValue(map.get(viewMode).get(type).get(representationType))) { + throw new Error(`There can't be more than one component to render Metadata of type "${type}" in view mode "${viewMode}" with representation type "${representationType}"`); + } + map.get(viewMode).get(type).set(representationType, component); }; } /** - * Get the component used for rendering an item by type and viewMode + * Get the component used for rendering an item by type and viewMode (and optionally a representationType) * @param type * @param viewMode + * @param representationType */ -export function getComponentByItemType(type: string, viewMode: ElementViewMode) { - let component = map.get(viewMode).get(type); - if (hasNoValue(component)) { - component = map.get(viewMode).get(DEFAULT_ITEM_TYPE); +export function getComponentByItemType(type: string, viewMode: string, representationType?: MetadataRepresentationType) { + if (hasNoValue(representationType)) { + representationType = NO_REPRESENTATION_TYPE; } - return component; + if (hasNoValue(map.get(viewMode))) { + viewMode = DEFAULT_VIEW_MODE; + } + if (hasNoValue(map.get(viewMode).get(type))) { + type = DEFAULT_ITEM_TYPE; + } + let representationComponent = map.get(viewMode).get(type).get(representationType); + if (hasNoValue(representationComponent)) { + representationComponent = map.get(viewMode).get(type).get(DEFAULT_REPRESENTATION_TYPE); + } + if (hasNoValue(representationComponent)) { + representationComponent = map.get(viewMode).get(type).get(NO_REPRESENTATION_TYPE); + } + return representationComponent; } diff --git a/src/app/shared/items/switcher/item-type-switcher.component.spec.ts b/src/app/shared/items/switcher/item-type-switcher.component.spec.ts index 67f5309793..cb657b1625 100644 --- a/src/app/shared/items/switcher/item-type-switcher.component.spec.ts +++ b/src/app/shared/items/switcher/item-type-switcher.component.spec.ts @@ -8,8 +8,10 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { RemoteData } from '../../../core/data/remote-data'; import * as decorator from '../item-type-decorator'; import { getComponentByItemType } from '../item-type-decorator'; -import { ElementViewMode } from '../../view-mode'; +import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import createSpy = jasmine.createSpy; +import { VIEW_MODE_FULL } from '../../../+item-page/simple/item-page.component'; +import { VIEW_MODE_METADATA } from '../../../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; const relationType = 'type'; const mockItem: Item = Object.assign(new Item(), { @@ -26,7 +28,8 @@ const mockItem: Item = Object.assign(new Item(), { value: relationType }] }); -const viewMode = ElementViewMode.Full; +const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(relationType), mockItem); +let viewMode = VIEW_MODE_FULL; describe('ItemTypeSwitcherComponent', () => { let comp: ItemTypeSwitcherComponent; @@ -47,13 +50,39 @@ describe('ItemTypeSwitcherComponent', () => { spyOnProperty(decorator, 'getComponentByItemType').and.returnValue(createSpy('getComponentByItemType')) })); - describe('when calling getComponent', () => { + describe('when the injected object is of type Item', () => { beforeEach(() => { - comp.getComponent(); + viewMode = VIEW_MODE_FULL; + comp.object = mockItem; + comp.viewMode = viewMode; }); - it('should call getComponentByItemType with parameters type and viewMode', () => { - expect(decorator.getComponentByItemType).toHaveBeenCalledWith(relationType, viewMode); + describe('when calling getComponent', () => { + beforeEach(() => { + comp.getComponent(); + }); + + it('should call getComponentByItemType with parameters type and viewMode', () => { + expect(decorator.getComponentByItemType).toHaveBeenCalledWith(relationType, viewMode); + }); + }); + }); + + describe('when the injected object is of type MetadataRepresentation', () => { + beforeEach(() => { + viewMode = VIEW_MODE_METADATA; + comp.object = mockItemMetadataRepresentation; + comp.viewMode = viewMode; + }); + + describe('when calling getComponent', () => { + beforeEach(() => { + comp.getComponent(); + }); + + it('should call getComponentByItemType with parameters type, viewMode and representationType', () => { + expect(decorator.getComponentByItemType).toHaveBeenCalledWith(relationType, viewMode, mockItemMetadataRepresentation.representationType); + }); }); }); diff --git a/src/app/shared/items/switcher/item-type-switcher.component.ts b/src/app/shared/items/switcher/item-type-switcher.component.ts index 5599326e98..2e03cb556c 100644 --- a/src/app/shared/items/switcher/item-type-switcher.component.ts +++ b/src/app/shared/items/switcher/item-type-switcher.component.ts @@ -4,7 +4,7 @@ import { Item } from '../../../core/shared/item.model'; import { hasValue } from '../../empty.util'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { getComponentByItemType } from '../item-type-decorator'; -import { ElementViewMode } from '../../view-mode'; +import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; export const ITEM: InjectionToken = new InjectionToken('item'); @@ -18,14 +18,14 @@ export const ITEM: InjectionToken = new InjectionToken('item'); */ export class ItemTypeSwitcherComponent implements OnInit { /** - * The item to determine the component for + * The item or metadata to determine the component for */ - @Input() object: Item | SearchResult; + @Input() object: Item | SearchResult | MetadataRepresentation; /** * The preferred view-mode to display */ - @Input() viewMode: ElementViewMode; + @Input() viewMode: string; /** * The object injector used to inject the item into the child component @@ -48,6 +48,11 @@ export class ItemTypeSwitcherComponent implements OnInit { * @returns {string} */ getComponent(): string { + if (hasValue((this.object as any).representationType)) { + const metadataRepresentation = this.object as MetadataRepresentation; + return getComponentByItemType(metadataRepresentation.itemType, this.viewMode, metadataRepresentation.representationType); + } + let item: Item; if (hasValue((this.object as any).dspaceObject)) { const searchResult = this.object as ItemSearchResult; 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 8cdf8cac6a..d433c7acf2 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 @@ -1 +1 @@ - + diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-list-element.component.ts index a9677c9777..4d34e75d61 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.ts @@ -1,10 +1,10 @@ import { Component } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import * as viewMode from '../../../shared/view-mode'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { SetViewMode } from '../../view-mode'; +import { VIEW_MODE_ELEMENT } from '../../../+item-page/simple/related-items/related-items-component'; @Component({ selector: 'ds-item-list-element', @@ -18,5 +18,5 @@ import { SetViewMode } from '../../view-mode'; */ @renderElementsFor(Item, SetViewMode.List) export class ItemListElementComponent extends AbstractListableElementComponent { - ElementViewMode = viewMode.ElementViewMode; + viewMode = VIEW_MODE_ELEMENT; } diff --git a/src/app/shared/object-list/item-list-element/item-types/journal-issue/journal-issue-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/journal-issue/journal-issue-list-element.component.ts index 4f18822773..3b1b9eb1c9 100644 --- a/src/app/shared/object-list/item-list-element/item-types/journal-issue/journal-issue-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/journal-issue/journal-issue-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('JournalIssue', ElementViewMode.SetElement) +@rendersItemType('JournalIssue', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-journal-issue-list-element', styleUrls: ['./journal-issue-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/journal-volume/journal-volume-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/journal-volume/journal-volume-list-element.component.ts index a95dca88f9..49a11b2cd9 100644 --- a/src/app/shared/object-list/item-list-element/item-types/journal-volume/journal-volume-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/journal-volume/journal-volume-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('JournalVolume', ElementViewMode.SetElement) +@rendersItemType('JournalVolume', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-journal-volume-list-element', styleUrls: ['./journal-volume-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/journal/journal-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/journal/journal-list-element.component.ts index 28be0d8149..45926c681c 100644 --- a/src/app/shared/object-list/item-list-element/item-types/journal/journal-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/journal/journal-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('Journal', ElementViewMode.SetElement) +@rendersItemType('Journal', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-journal-list-element', styleUrls: ['./journal-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/orgunit/orgunit-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/orgunit/orgunit-list-element.component.ts index e3433f7e76..699cb6dfab 100644 --- a/src/app/shared/object-list/item-list-element/item-types/orgunit/orgunit-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/orgunit/orgunit-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('OrgUnit', ElementViewMode.SetElement) +@rendersItemType('OrgUnit', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-orgunit-list-element', styleUrls: ['./orgunit-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.html b/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.html index 803674b56e..2d89fda483 100644 --- a/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.html @@ -13,4 +13,3 @@ - diff --git a/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.ts index b0369458f9..e12cc60813 100644 --- a/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/person/person-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('Person', ElementViewMode.SetElement) +@rendersItemType('Person', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-person-list-element', styleUrls: ['./person-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.html b/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.html new file mode 100644 index 0000000000..92e57d7ef4 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.ts new file mode 100644 index 0000000000..1f3d736d1c --- /dev/null +++ b/src/app/shared/object-list/item-list-element/item-types/person/person-metadata-list-element.component.ts @@ -0,0 +1,16 @@ +import { rendersItemType } from '../../../../items/item-type-decorator'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; +import { Component } from '@angular/core'; +import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { MetadataRepresentationType } from '../../../../../core/shared/metadata-representation/metadata-representation.model'; + +@rendersItemType('Person', VIEW_MODE_ELEMENT, MetadataRepresentationType.Item) +@Component({ + selector: 'ds-person-metadata-list-element', + templateUrl: './person-metadata-list-element.component.html' +}) +/** + * The component for displaying a list element for an item of the type Person + */ +export class PersonMetadataListElementComponent extends TypedItemSearchResultListElementComponent { +} diff --git a/src/app/shared/object-list/item-list-element/item-types/project/project-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/project/project-list-element.component.ts index 6b130e4786..e688056bc6 100644 --- a/src/app/shared/object-list/item-list-element/item-types/project/project-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/project/project-list-element.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; import { rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('Project', ElementViewMode.SetElement) +@rendersItemType('Project', VIEW_MODE_ELEMENT) @Component({ selector: 'ds-project-list-element', styleUrls: ['./project-list-element.component.scss'], diff --git a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.ts index 4575ab5cca..58a0568931 100644 --- a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.ts @@ -1,10 +1,10 @@ import { Component } from '@angular/core'; import { DEFAULT_ITEM_TYPE, rendersItemType } from '../../../../items/item-type-decorator'; -import { ElementViewMode } from '../../../../view-mode'; import { TypedItemSearchResultListElementComponent } from '../typed-item-search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../../+item-page/simple/related-items/related-items-component'; -@rendersItemType('Publication', ElementViewMode.SetElement) -@rendersItemType(DEFAULT_ITEM_TYPE, ElementViewMode.SetElement) +@rendersItemType('Publication', VIEW_MODE_ELEMENT) +@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_ELEMENT) @Component({ selector: 'ds-publication-list-element', styleUrls: ['./publication-list-element.component.scss'], diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html new file mode 100644 index 0000000000..764fdc1064 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html @@ -0,0 +1,2 @@ + + diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..90de549800 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ItemMetadataListElementComponent } from './item-metadata-list-element.component'; +import { By } from '@angular/platform-browser'; +import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; + +const mockItemMetadataRepresentation = new ItemMetadataRepresentation('type'); + +describe('ItemMetadataListElementComponent', () => { + let comp: ItemMetadataListElementComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [ItemMetadataListElementComponent], + providers: [ + { provide: ITEM, useValue: mockItemMetadataRepresentation } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemMetadataListElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(ItemMetadataListElementComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should call an item-type-switcher component and pass the item-metadata-representation', () => { + const itemTypeSwitcher = fixture.debugElement.query(By.css('ds-item-type-switcher')).nativeElement; + expect(itemTypeSwitcher.object).toBe(mockItemMetadataRepresentation); + }); + +}); diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.ts new file mode 100644 index 0000000000..a5900cc763 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.ts @@ -0,0 +1,25 @@ +import * as viewMode from '../../../view-mode'; +import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { Component } from '@angular/core'; +import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; +import { DEFAULT_ITEM_TYPE, rendersItemType } from '../../../items/item-type-decorator'; +import { VIEW_MODE_METADATA } from '../../../../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; +import { VIEW_MODE_ELEMENT } from '../../../../+item-page/simple/related-items/related-items-component'; + +@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_METADATA, MetadataRepresentationType.Item) +@Component({ + selector: 'ds-item-metadata-list-element', + templateUrl: './item-metadata-list-element.component.html' +}) +/** + * A component for displaying MetadataRepresentation objects in the form of items + * It will send the MetadataRepresentation object along with ElementViewMode.SetElement to the ItemTypeSwitcherComponent, + * which will in his turn decide how to render the item as metadata. + */ +export class ItemMetadataListElementComponent extends MetadataRepresentationListElementComponent { + /** + * The view-mode we're currently on + * @type {ElementViewMode} + */ + viewMode = VIEW_MODE_ELEMENT; +} diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts new file mode 100644 index 0000000000..2488db50b1 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts @@ -0,0 +1,15 @@ +import { Component, Inject } from '@angular/core'; +import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; +import { ITEM } from '../../items/switcher/item-type-switcher.component'; + +@Component({ + selector: 'ds-metadata-representation-list-element', + template: '' +}) +/** + * An abstract class for displaying a single MetadataRepresentation + */ +export class MetadataRepresentationListElementComponent { + constructor(@Inject(ITEM) public metadataRepresentation: MetadataRepresentation) { + } +} diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html new file mode 100644 index 0000000000..2533cf834b --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -0,0 +1,3 @@ +
+ {{metadataRepresentation.getPrimaryValue()}} +
diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..42b9abde16 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts @@ -0,0 +1,39 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { PlainTextMetadataListElementComponent } from './plain-text-metadata-list-element.component'; +import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; + +const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), { + key: 'dc.contributor.author', + value: 'Test Author' +}); + +describe('PlainTextMetadataListElementComponent', () => { + let comp: PlainTextMetadataListElementComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [PlainTextMetadataListElementComponent], + providers: [ + { provide: ITEM, useValue: mockMetadataRepresentation } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(PlainTextMetadataListElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(PlainTextMetadataListElementComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should contain the value as plain text', () => { + expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value); + }); + +}); diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts new file mode 100644 index 0000000000..e7cf235c75 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -0,0 +1,19 @@ +import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { Component } from '@angular/core'; +import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; +import { DEFAULT_ITEM_TYPE, rendersItemType } from '../../../items/item-type-decorator'; +import { VIEW_MODE_METADATA } from '../../../../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; + +@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_METADATA, MetadataRepresentationType.PlainText) +// For now, authority controlled fields are rendered the same way as plain text fields +@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_METADATA, MetadataRepresentationType.AuthorityControlled) +@Component({ + selector: 'ds-plain-text-metadata-list-element', + templateUrl: './plain-text-metadata-list-element.component.html' +}) +/** + * A component for displaying MetadataRepresentation objects in the form of plain text + * It will simply use the value retrieved from MetadataRepresentation.getPrimaryValue() to display as plain text + */ +export class PlainTextMetadataListElementComponent extends MetadataRepresentationListElementComponent { +} 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 8cdf8cac6a..d433c7acf2 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 @@ -1 +1 @@ - + diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts index b9ac52ca40..a164e5a65b 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts @@ -4,9 +4,9 @@ import { focusBackground } from '../../../animations/focus'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import * as viewMode from '../../../../shared/view-mode'; import { SetViewMode } from '../../../view-mode'; import { SearchResultListElementComponent } from '../search-result-list-element.component'; +import { VIEW_MODE_ELEMENT } from '../../../../+item-page/simple/related-items/related-items-component'; @Component({ selector: 'ds-item-search-result-list-element', @@ -18,5 +18,5 @@ import { SearchResultListElementComponent } from '../search-result-list-element. @renderElementsFor(ItemSearchResult, SetViewMode.List) export class ItemSearchResultListElementComponent extends SearchResultListElementComponent { - ElementViewMode = viewMode.ElementViewMode; + viewMode = VIEW_MODE_ELEMENT; } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index e467839b57..1c40e86680 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -94,6 +94,11 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { MomentModule } from 'ngx-moment'; import { NouisliderModule } from 'ng2-nouislider'; +import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; +import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; +import { TooltipModule } from 'ngx-bootstrap'; +import { PersonMetadataListElementComponent } from './object-list/item-list-element/item-types/person/person-metadata-list-element.component'; +import { MetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/metadata-representation-list-element.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -117,6 +122,10 @@ const MODULES = [ TextMaskModule ]; +const ROOT_MODULES = [ + TooltipModule.forRoot() +]; + const PIPES = [ // put shared pipes here EnumKeysPipe, @@ -184,12 +193,16 @@ const ENTRY_COMPONENTS = [ SearchResultGridElementComponent, PublicationListElementComponent, PersonListElementComponent, + PersonMetadataListElementComponent, OrgUnitListElementComponent, ProjectListElementComponent, JournalListElementComponent, JournalVolumeListElementComponent, JournalIssueListElementComponent, - BrowseEntryListElementComponent + BrowseEntryListElementComponent, + PlainTextMetadataListElementComponent, + ItemMetadataListElementComponent, + MetadataRepresentationListElementComponent ]; const PROVIDERS = [ @@ -206,7 +219,8 @@ const DIRECTIVES = [ @NgModule({ imports: [ - ...MODULES + ...MODULES, + ...ROOT_MODULES ], declarations: [ ...PIPES, diff --git a/src/app/shared/view-mode.ts b/src/app/shared/view-mode.ts index 2825f20c50..d36764a6e8 100644 --- a/src/app/shared/view-mode.ts +++ b/src/app/shared/view-mode.ts @@ -8,17 +8,7 @@ export enum SetViewMode { Grid = 'grid' } -/** - * Enum used for defining the view-mode of a single element - * Full Display the full element - * SetElement Display the element as part of a set - */ -export enum ElementViewMode { - Full, - SetElement -} - /** * ViewMode refers to either a SetViewMode or ElementViewMode */ -export type ViewMode = SetViewMode | ElementViewMode; +export type ViewMode = SetViewMode;