diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 7838bfdd25..208e5415a8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -388,7 +388,21 @@ } }, "browse": { - "title": "Browsing {{ collection }} by {{ field }} {{ value }}" + "title": "Browsing {{ collection }} by {{ field }} {{ value }}", + "metadata": { + "title": "Title", + "author": "Author", + "subject": "Subject" + }, + "comcol": { + "head": "Browse", + "by": { + "title": "By Title", + "author": "By Author", + "subject": "By Subject" + } + }, + "empty": "No items to show." }, "admin": { "registries": { @@ -519,6 +533,7 @@ "browse_global_by_issue_date": "By Issue Date", "browse_global_by_author": "By Author", "browse_global_by_title": "By Title", + "browse_global_by_subject": "By Subject", "statistics": "Statistics", "browse_community": "This Community", "browse_community_by_issue_date": "By Issue Date", diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts index 3c68157a3e..f335c880ae 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts @@ -32,6 +32,11 @@ const initialState: MetadataRegistryState = { selectedFields: [] }; +/** + * Reducer that handles MetadataRegistryActions to modify metadata schema and/or field states + * @param state The current MetadataRegistryState + * @param action The MetadataRegistryAction to perform on the state + */ export function metadataRegistryReducer(state = initialState, action: MetadataRegistryAction): MetadataRegistryState { switch (action.type) { diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index 509af6609b..4c09428bd5 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -95,6 +95,9 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy { private translateService: TranslateService) { } + /** + * Initialize the component, setting up the necessary Models for the dynamic form + */ ngOnInit() { combineLatest( this.translateService.get(`${this.messagePrefix}.element`), diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.html b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.html deleted file mode 100644 index 438c318994..0000000000 --- a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
{{metadatum.key}} | -{{metadatum.value}} | -{{metadatum.language}} | -
{{mdEntry.key}} | +{{mdValue.value}} | +{{mdValue.language}} | +
This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
' - }, - { - key: 'dc.description.abstract', - language: null, - value: 'This is a sample top-level community' - }, - { - key: 'dc.description.tableofcontents', - language: null, - value: 'This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
' - }, - { - key: 'dc.rights', - language: null, - value: 'If this Community had special copyright text to display, it would be displayed here.
' - }, - { - key: 'dc.title', - language: null, - value: 'Sample Community' - } - ] + metadata: { + 'dc.description': [ + { + language: null, + value: 'This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
' + } + ], + 'dc.description.abstract': [ + { + language: null, + value: 'This is a sample top-level community' + } + ], + 'dc.description.tableofcontents': [ + { + language: null, + value: 'This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
' + } + ], + 'dc.rights': [ + { + language: null, + value: 'If this Community had special copyright text to display, it would be displayed here.
' + } + ], + 'dc.title': [ + { + language: null, + value: 'Sample Community' + } + ] + } } ) ]; diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index a4131db489..ee9f2e571b 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -60,23 +60,26 @@ describe('AuthResponseParsingService', () => { handle: null, id: '4dc70ab5-cd73-492f-b007-3179d2d9296b', lastActive: '2018-05-14T17:03:31.277+0000', - metadata: [ - { - key: 'eperson.firstname', - language: null, - value: 'User' - }, - { - key: 'eperson.lastname', - language: null, - value: 'Test' - }, - { - key: 'eperson.language', - language: null, - value: 'en' - } - ], + metadata: { + 'eperson.firstname': [ + { + language: null, + value: 'User' + } + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test' + } + ], + 'eperson.language': [ + { + language: null, + value: 'en' + } + ] + }, name: 'User Test', netid: 'myself@testshib.org', requireCertificate: false, diff --git a/src/app/core/browse/browse-entry-search-options.model.ts b/src/app/core/browse/browse-entry-search-options.model.ts new file mode 100644 index 0000000000..a4911a33f1 --- /dev/null +++ b/src/app/core/browse/browse-entry-search-options.model.ts @@ -0,0 +1,17 @@ +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortOptions } from '../cache/models/sort-options.model'; + +/** + * A class that defines the search options to be used for fetching browse entries or items + * - metadataDefinition: The metadata definition to fetch entries or items for + * - pagination: Optional pagination options to use + * - sort: Optional sorting options to use + * - scope: An optional scope to limit the results within a specific collection or community + */ +export class BrowseEntrySearchOptions { + constructor(public metadataDefinition: string, + public pagination?: PaginationComponentOptions, + public sort?: SortOptions, + public scope?: string) { + } +} diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index da75e1a877..d37a87c69e 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -8,6 +8,7 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseService } from './browse.service'; +import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { RequestEntry } from '../data/request.reducer'; import { of as observableOf } from 'rxjs'; @@ -151,14 +152,14 @@ describe('BrowseService', () => { it('should configure a new BrowseEntriesRequest', () => { const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries); - scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe()); + scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getBrowseEntriesFor(browseDefinitions[1].id); + service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); @@ -170,14 +171,14 @@ describe('BrowseService', () => { it('should configure a new BrowseItemsRequest', () => { const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName); - scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe()); + scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName); + service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); @@ -191,7 +192,7 @@ describe('BrowseService', () => { const definitionID = 'invalidID'; const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)) - expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected); + expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected); }); }); @@ -201,7 +202,7 @@ describe('BrowseService', () => { const definitionID = 'invalidID'; const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)) - expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected); + expect(service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected); }); }); }); @@ -219,44 +220,44 @@ describe('BrowseService', () => { }})); }); - it('should return the URL for the given metadatumKey and linkPath', () => { - const metadatumKey = 'dc.date.issued'; + it('should return the URL for the given metadataKey and linkPath', () => { + const metadataKey = 'dc.date.issued'; const linkPath = 'items'; const expectedURL = browseDefinitions[0]._links[linkPath]; - const result = service.getBrowseURLFor(metadatumKey, linkPath); + const result = service.getBrowseURLFor(metadataKey, linkPath); const expected = cold('c-d-', { c: undefined, d: expectedURL }); expect(result).toBeObservable(expected); }); - it('should work when the definition uses a wildcard in the metadatumKey', () => { - const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition + it('should work when the definition uses a wildcard in the metadataKey', () => { + const metadataKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition const linkPath = 'items'; const expectedURL = browseDefinitions[1]._links[linkPath]; - const result = service.getBrowseURLFor(metadatumKey, linkPath); + const result = service.getBrowseURLFor(metadataKey, linkPath); const expected = cold('c-d-', { c: undefined, d: expectedURL }); expect(result).toBeObservable(expected); }); it('should throw an error when the key doesn\'t match', () => { - const metadatumKey = 'dc.title'; // isn't in the definitions + const metadataKey = 'dc.title'; // isn't in the definitions const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkPath); - const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadataKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`)); expect(result).toBeObservable(expected); }); it('should throw an error when the link doesn\'t match', () => { - const metadatumKey = 'dc.date.issued'; + const metadataKey = 'dc.date.issued'; const linkPath = 'collections'; // isn't in the definitions - const result = service.getBrowseURLFor(metadatumKey, linkPath); - const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadataKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`)); expect(result).toBeObservable(expected); }); @@ -271,10 +272,10 @@ describe('BrowseService', () => { spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('----')); - const metadatumKey = 'dc.date.issued'; + const metadataKey = 'dc.date.issued'; const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkPath); + const result = service.getBrowseURLFor(metadataKey, linkPath); const expected = cold('b---', { b: undefined }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b807a77e99..ef4fdaa5ff 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -33,13 +33,17 @@ import { import { URLCombiner } from '../url-combiner/url-combiner'; import { Item } from '../shared/item.model'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; +/** + * Service that performs all actions that have to do with browse. + */ @Injectable() export class BrowseService { protected linkPath = 'browses'; - private static toSearchKeyArray(metadatumKey: string): string[] { - const keyParts = metadatumKey.split('.'); + private static toSearchKeyArray(metadataKey: string): string[] { + const keyParts = metadataKey.split('.'); const searchFor = []; searchFor.push('*'); for (let i = 0; i < keyParts.length - 1; i++) { @@ -47,7 +51,7 @@ export class BrowseService { const nextPart = [...prevParts, '*'].join('.'); searchFor.push(nextPart); } - searchFor.push(metadatumKey); + searchFor.push(metadataKey); return searchFor; } @@ -80,18 +84,18 @@ export class BrowseService { return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } - getBrowseEntriesFor(definitionID: string, options: { - pagination?: PaginationComponentOptions; - sort?: SortOptions; - } = {}): Observable0" class="item-authors card-text text-muted"> - {{authorMd.value}} +
{{object.findMetadata("dc.description.abstract") }}
+{{object.firstMetadataValue("dc.description.abstract")}}
0" +
This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
' - }, - { - key: 'dc.description.abstract', - language: null, - value: 'This is a sample top-level community' - }, - { - key: 'dc.description.tableofcontents', - language: null, - value: 'This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
' - }, - { - key: 'dc.rights', - language: null, - value: 'If this Community had special copyright text to display, it would be displayed here.
' - }, - { - key: 'dc.title', - language: null, - value: 'Sample Community' - } - ] + metadata: { + 'dc.description': [ + { + language: null, + value: 'This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
' + } + ], + 'dc.description.abstract': [ + { + language: null, + value: 'This is a sample top-level community' + } + ], + 'dc.description.tableofcontents': [ + { + language: null, + value: 'This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
' + } + ], + 'dc.rights': [ + { + language: null, + value: 'If this Community had special copyright text to display, it would be displayed here.
' + } + ], + 'dc.title': [ + { + language: null, + value: 'Sample Community' + } + ] + } } ) ]; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 3a2a676a1c..5ae3e517e3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -94,6 +94,7 @@ import { LangSwitchComponent } from './lang-switch/lang-switch.component'; import { ObjectValuesPipe } from './utils/object-values-pipe'; import { InListValidator } from './utils/in-list-validator.directive'; import { AutoFocusDirective } from './utils/auto-focus.directive'; +import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -142,6 +143,7 @@ const COMPONENTS = [ CreateComColPageComponent, EditComColPageComponent, DeleteComColPageComponent, + ComcolPageBrowseByComponent, DsDynamicFormComponent, DsDynamicFormControlComponent, DsDynamicListComponent,