diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index 819f8ad30a..93fc7e1ead 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -342,7 +342,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": {
@@ -439,6 +453,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/+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 @@
-
diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts
deleted file mode 100644
index 813ee8a32f..0000000000
--- a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-
-import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
-import { Component, OnInit } from '@angular/core';
-import { RemoteData } from '../../core/data/remote-data';
-import { PaginatedList } from '../../core/data/paginated-list';
-import { ItemDataService } from '../../core/data/item-data.service';
-import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
-import { ActivatedRoute } from '@angular/router';
-import { hasValue, isNotEmpty } from '../../shared/empty.util';
-import { BrowseService } from '../../core/browse/browse.service';
-import { BrowseEntry } from '../../core/shared/browse-entry.model';
-import { Item } from '../../core/shared/item.model';
-
-@Component({
- selector: 'ds-browse-by-author-page',
- styleUrls: ['./browse-by-author-page.component.scss'],
- templateUrl: './browse-by-author-page.component.html'
-})
-/**
- * Component for browsing (items) by author (dc.contributor.author)
- */
-export class BrowseByAuthorPageComponent implements OnInit {
-
- authors$: Observable>>;
- items$: Observable>>;
- paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
- id: 'browse-by-author-pagination',
- currentPage: 1,
- pageSize: 20
- });
- sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
- subs: Subscription[] = [];
- currentUrl: string;
- value = '';
-
- public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
- }
-
- ngOnInit(): void {
- this.currentUrl = this.route.snapshot.pathFromRoot
- .map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
- .join('/');
- this.updatePage({
- pagination: this.paginationConfig,
- sort: this.sortConfig
- });
- this.subs.push(
- observableCombineLatest(
- this.route.params,
- this.route.queryParams,
- (params, queryParams, ) => {
- return Object.assign({}, params, queryParams);
- })
- .subscribe((params) => {
- const page = +params.page || this.paginationConfig.currentPage;
- const pageSize = +params.pageSize || this.paginationConfig.pageSize;
- const sortDirection = params.sortDirection || this.sortConfig.direction;
- const sortField = params.sortField || this.sortConfig.field;
- this.value = +params.value || params.value || '';
- const pagination = Object.assign({},
- this.paginationConfig,
- { currentPage: page, pageSize: pageSize }
- );
- const sort = Object.assign({},
- this.sortConfig,
- { direction: sortDirection, field: sortField }
- );
- const searchOptions = {
- pagination: pagination,
- sort: sort
- };
- if (isNotEmpty(this.value)) {
- this.updatePageWithItems(searchOptions, this.value);
- } else {
- this.updatePage(searchOptions);
- }
- }));
- }
-
- /**
- * Updates the current page with searchOptions
- * @param searchOptions Options to narrow down your search:
- * { pagination: PaginationComponentOptions,
- * sort: SortOptions }
- */
- updatePage(searchOptions) {
- this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
- this.items$ = undefined;
- }
-
- /**
- * Updates the current page with searchOptions and display items linked to author
- * @param searchOptions Options to narrow down your search:
- * { pagination: PaginationComponentOptions,
- * sort: SortOptions }
- * @param author The author's name for displaying items
- */
- updatePageWithItems(searchOptions, author: string) {
- this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
- }
-
- ngOnDestroy(): void {
- this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
- }
-
-}
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html
new file mode 100644
index 0000000000..08fb762db0
--- /dev/null
+++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html
@@ -0,0 +1,10 @@
+
diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.scss b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.scss
similarity index 100%
rename from src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.scss
rename to src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.scss
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts
new file mode 100644
index 0000000000..a53faf6b8b
--- /dev/null
+++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts
@@ -0,0 +1,162 @@
+import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowseService } from '../../core/browse/browse.service';
+import { CommonModule } from '@angular/common';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
+import { ActivatedRoute, Router } from '@angular/router';
+import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { Observable } from 'rxjs/internal/Observable';
+import { RemoteData } from '../../core/data/remote-data';
+import { PaginatedList } from '../../core/data/paginated-list';
+import { PageInfo } from '../../core/shared/page-info.model';
+import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
+import { SortDirection } from '../../core/cache/models/sort-options.model';
+import { Item } from '../../core/shared/item.model';
+import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { Community } from '../../core/shared/community.model';
+
+describe('BrowseByMetadataPageComponent', () => {
+ let comp: BrowseByMetadataPageComponent;
+ let fixture: ComponentFixture;
+ let browseService: BrowseService;
+ let route: ActivatedRoute;
+
+ const mockCommunity = Object.assign(new Community(), {
+ id: 'test-uuid',
+ metadata: [
+ {
+ key: 'dc.title',
+ value: 'test community'
+ }
+ ]
+ });
+
+ const mockEntries = [
+ {
+ type: 'author',
+ authority: null,
+ value: 'John Doe',
+ language: 'en',
+ count: 1
+ },
+ {
+ type: 'author',
+ authority: null,
+ value: 'James Doe',
+ language: 'en',
+ count: 3
+ },
+ {
+ type: 'subject',
+ authority: null,
+ value: 'Fake subject',
+ language: 'en',
+ count: 2
+ }
+ ];
+
+ const mockItems = [
+ Object.assign(new Item(), {
+ id: 'fakeId'
+ })
+ ];
+
+ const mockBrowseService = {
+ getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData(mockEntries.filter((entry) => entry.type === options.metadataDefinition)),
+ getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData(mockItems)
+ };
+
+ const mockDsoService = {
+ findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
+ };
+
+ const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
+ params: observableOf({})
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [BrowseByMetadataPageComponent, EnumKeysPipe],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRouteStub },
+ { provide: BrowseService, useValue: mockBrowseService },
+ { provide: DSpaceObjectDataService, useValue: mockDsoService }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BrowseByMetadataPageComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ browseService = (comp as any).browseService;
+ route = (comp as any).route;
+ route.params = observableOf({});
+ comp.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should fetch the correct entries depending on the metadata definition', () => {
+ comp.browseEntries$.subscribe((result) => {
+ expect(result.payload.page).toEqual(mockEntries.filter((entry) => entry.type === 'author'));
+ });
+ });
+
+ it('should not fetch any items when no value is provided', () => {
+ expect(comp.items$).toBeUndefined();
+ });
+
+ describe('when a value is provided', () => {
+ beforeEach(() => {
+ const paramsWithValue = {
+ metadata: 'author',
+ value: 'John Doe'
+ };
+
+ route.params = observableOf(paramsWithValue);
+ comp.ngOnInit();
+ });
+
+ it('should fetch items', () => {
+ comp.items$.subscribe((result) => {
+ expect(result.payload.page).toEqual(mockItems);
+ });
+ })
+ });
+
+ describe('when calling browseParamsToOptions', () => {
+ let result: BrowseEntrySearchOptions;
+
+ beforeEach(() => {
+ const paramsWithPaginationAndScope = {
+ page: 5,
+ pageSize: 10,
+ sortDirection: SortDirection.ASC,
+ sortField: 'fake-field',
+ scope: 'fake-scope'
+ };
+
+ result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author');
+ });
+
+ it('should return BrowseEntrySearchOptions with the correct properties', () => {
+ expect(result.metadataDefinition).toEqual('author');
+ expect(result.pagination.currentPage).toEqual(5);
+ expect(result.pagination.pageSize).toEqual(10);
+ expect(result.sort.direction).toEqual(SortDirection.ASC);
+ expect(result.sort.field).toEqual('fake-field');
+ expect(result.scope).toEqual('fake-scope');
+ })
+ });
+});
+
+export function toRemoteData(objects: any[]): Observable>> {
+ return observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), objects)));
+}
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts
new file mode 100644
index 0000000000..87ccb20c0b
--- /dev/null
+++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts
@@ -0,0 +1,182 @@
+import {combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
+import { Component, OnInit } from '@angular/core';
+import { RemoteData } from '../../core/data/remote-data';
+import { PaginatedList } from '../../core/data/paginated-list';
+import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
+import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
+import { ActivatedRoute } from '@angular/router';
+import { hasValue, isNotEmpty } from '../../shared/empty.util';
+import { BrowseService } from '../../core/browse/browse.service';
+import { BrowseEntry } from '../../core/shared/browse-entry.model';
+import { Item } from '../../core/shared/item.model';
+import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
+import { Community } from '../../core/shared/community.model';
+import { Collection } from '../../core/shared/collection.model';
+import { getSucceededRemoteData } from '../../core/shared/operators';
+import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { DSpaceObject } from '../../core/shared/dspace-object.model';
+
+@Component({
+ selector: 'ds-browse-by-metadata-page',
+ styleUrls: ['./browse-by-metadata-page.component.scss'],
+ templateUrl: './browse-by-metadata-page.component.html'
+})
+/**
+ * Component for browsing (items) by metadata definition
+ * A metadata definition is a short term used to describe one or multiple metadata fields.
+ * An example would be 'author' for 'dc.contributor.*'
+ */
+export class BrowseByMetadataPageComponent implements OnInit {
+
+ /**
+ * The list of browse-entries to display
+ */
+ browseEntries$: Observable>>;
+
+ /**
+ * The list of items to display when a value is present
+ */
+ items$: Observable>>;
+
+ /**
+ * The current Community or Collection we're browsing metadata/items in
+ */
+ parent$: Observable>;
+
+ /**
+ * The pagination config used to display the values
+ */
+ paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'browse-by-metadata-pagination',
+ currentPage: 1,
+ pageSize: 20
+ });
+
+ /**
+ * The sorting config used to sort the values (defaults to Ascending)
+ */
+ sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC);
+
+ /**
+ * List of subscriptions
+ */
+ subs: Subscription[] = [];
+
+ /**
+ * The default metadata definition to resort to when none is provided
+ */
+ defaultMetadata = 'author';
+
+ /**
+ * The current metadata definition
+ */
+ metadata = this.defaultMetadata;
+
+ /**
+ * The value we're browing items for
+ * - When the value is not empty, we're browsing items
+ * - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
+ */
+ value = '';
+
+ public constructor(private route: ActivatedRoute,
+ private browseService: BrowseService,
+ private dsoService: DSpaceObjectDataService) {
+ }
+
+ ngOnInit(): void {
+ this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
+ this.subs.push(
+ observableCombineLatest(
+ this.route.params,
+ this.route.queryParams,
+ (params, queryParams, ) => {
+ return Object.assign({}, params, queryParams);
+ })
+ .subscribe((params) => {
+ this.metadata = params.metadata || this.defaultMetadata;
+ this.value = +params.value || params.value || '';
+ const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
+ if (isNotEmpty(this.value)) {
+ this.updatePageWithItems(searchOptions, this.value);
+ } else {
+ this.updatePage(searchOptions);
+ }
+ this.updateParent(params.scope);
+ }));
+ }
+
+ /**
+ * Updates the current page with searchOptions
+ * @param searchOptions Options to narrow down your search:
+ * { metadata: string
+ * pagination: PaginationComponentOptions,
+ * sort: SortOptions,
+ * scope: string }
+ */
+ updatePage(searchOptions: BrowseEntrySearchOptions) {
+ this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions);
+ this.items$ = undefined;
+ }
+
+ /**
+ * Updates the current page with searchOptions and display items linked to the given value
+ * @param searchOptions Options to narrow down your search:
+ * { metadata: string
+ * pagination: PaginationComponentOptions,
+ * sort: SortOptions,
+ * scope: string }
+ * @param value The value of the browse-entry to display items for
+ */
+ updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
+ this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
+ }
+
+ /**
+ * Update the parent Community or Collection using their scope
+ * @param scope The UUID of the Community or Collection to fetch
+ */
+ updateParent(scope: string) {
+ if (hasValue(scope)) {
+ this.parent$ = this.dsoService.findById(scope).pipe(
+ getSucceededRemoteData()
+ );
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
+ }
+
+}
+
+/**
+ * Function to transform query and url parameters into searchOptions used to fetch browse entries or items
+ * @param params URL and query parameters
+ * @param paginationConfig Pagination configuration
+ * @param sortConfig Sorting configuration
+ * @param metadata Optional metadata definition to fetch browse entries/items for
+ */
+export function browseParamsToOptions(params: any,
+ paginationConfig: PaginationComponentOptions,
+ sortConfig: SortOptions,
+ metadata?: string): BrowseEntrySearchOptions {
+ return new BrowseEntrySearchOptions(
+ metadata,
+ Object.assign({},
+ paginationConfig,
+ {
+ currentPage: +params.page || paginationConfig.currentPage,
+ pageSize: +params.pageSize || paginationConfig.pageSize
+ }
+ ),
+ Object.assign({},
+ sortConfig,
+ {
+ direction: params.sortDirection || sortConfig.direction,
+ field: params.sortField || sortConfig.field
+ }
+ ),
+ params.scope
+ );
+}
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html
index d37727be36..84b0baf1f6 100644
--- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html
+++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html
@@ -1,9 +1,8 @@
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts
new file mode 100644
index 0000000000..c92e5c64cb
--- /dev/null
+++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts
@@ -0,0 +1,85 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { Item } from '../../core/shared/item.model';
+import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { CommonModule } from '@angular/common';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
+import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
+import { ItemDataService } from '../../core/data/item-data.service';
+import { Community } from '../../core/shared/community.model';
+import { RemoteData } from '../../core/data/remote-data';
+import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+
+describe('BrowseByTitlePageComponent', () => {
+ let comp: BrowseByTitlePageComponent;
+ let fixture: ComponentFixture
;
+ let itemDataService: ItemDataService;
+ let route: ActivatedRoute;
+
+ const mockCommunity = Object.assign(new Community(), {
+ id: 'test-uuid',
+ metadata: [
+ {
+ key: 'dc.title',
+ value: 'test community'
+ }
+ ]
+ });
+
+ const mockItems = [
+ Object.assign(new Item(), {
+ id: 'fakeId',
+ metadata: [
+ {
+ key: 'dc.title',
+ value: 'Fake Title'
+ }
+ ]
+ })
+ ];
+
+ const mockItemDataService = {
+ findAll: () => toRemoteData(mockItems)
+ };
+
+ const mockDsoService = {
+ findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
+ };
+
+ const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
+ params: observableOf({})
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRouteStub },
+ { provide: ItemDataService, useValue: mockItemDataService },
+ { provide: DSpaceObjectDataService, useValue: mockDsoService }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BrowseByTitlePageComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ itemDataService = (comp as any).itemDataService;
+ route = (comp as any).route;
+ });
+
+ it('should initialize the list of items', () => {
+ comp.items$.subscribe((result) => {
+ expect(result.payload.page).toEqual(mockItems);
+ });
+ });
+});
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts
index e9127dbbab..6ba43c8f10 100644
--- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts
+++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts
@@ -1,16 +1,20 @@
-
-import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
+import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
-import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
-import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
+import { ActivatedRoute } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
+import { browseParamsToOptions } from '../+browse-by-metadata-page/browse-by-metadata-page.component';
+import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
+import { Community } from '../../core/shared/community.model';
+import { getSucceededRemoteData } from '../../core/shared/operators';
+import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
+import { DSpaceObject } from '../../core/shared/dspace-object.model';
@Component({
selector: 'ds-browse-by-title-page',
@@ -22,28 +26,44 @@ import { Collection } from '../../core/shared/collection.model';
*/
export class BrowseByTitlePageComponent implements OnInit {
+ /**
+ * The list of items to display
+ */
items$: Observable>>;
+
+ /**
+ * The current Community or Collection we're browsing metadata/items in
+ */
+ parent$: Observable>;
+
+ /**
+ * The pagination configuration to use for displaying the list of items
+ */
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
- sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
- subs: Subscription[] = [];
- currentUrl: string;
- public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
+ /**
+ * The sorting configuration to use for displaying the list of items
+ * Sorted by title (Ascending by default)
+ */
+ sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
+
+ /**
+ * List of subscriptions
+ */
+ subs: Subscription[] = [];
+
+ public constructor(private itemDataService: ItemDataService,
+ private route: ActivatedRoute,
+ private dsoService: DSpaceObjectDataService) {
}
ngOnInit(): void {
- this.currentUrl = this.route.snapshot.pathFromRoot
- .map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
- .join('/');
- this.updatePage({
- pagination: this.paginationConfig,
- sort: this.sortConfig
- });
+ this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
this.subs.push(
observableCombineLatest(
this.route.params,
@@ -52,22 +72,8 @@ export class BrowseByTitlePageComponent implements OnInit {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
- const page = +params.page || this.paginationConfig.currentPage;
- const pageSize = +params.pageSize || this.paginationConfig.pageSize;
- const sortDirection = params.sortDirection || this.sortConfig.direction;
- const sortField = params.sortField || this.sortConfig.field;
- const pagination = Object.assign({},
- this.paginationConfig,
- { currentPage: page, pageSize: pageSize }
- );
- const sort = Object.assign({},
- this.sortConfig,
- { direction: sortDirection, field: sortField }
- );
- this.updatePage({
- pagination: pagination,
- sort: sort
- });
+ this.updatePage(browseParamsToOptions(params, this.paginationConfig, this.sortConfig));
+ this.updateParent(params.scope)
}));
}
@@ -77,14 +83,27 @@ export class BrowseByTitlePageComponent implements OnInit {
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
- updatePage(searchOptions) {
+ updatePage(searchOptions: BrowseEntrySearchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
- sort: searchOptions.sort
+ sort: searchOptions.sort,
+ scopeID: searchOptions.scope
});
}
+ /**
+ * Update the parent Community or Collection using their scope
+ * @param scope The UUID of the Community or Collection to fetch
+ */
+ updateParent(scope: string) {
+ if (hasValue(scope)) {
+ this.parent$ = this.dsoService.findById(scope).pipe(
+ getSucceededRemoteData()
+ );
+ }
+ }
+
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
diff --git a/src/app/+browse-by/browse-by-routing.module.ts b/src/app/+browse-by/browse-by-routing.module.ts
index 630a7c0db5..38915fffca 100644
--- a/src/app/+browse-by/browse-by-routing.module.ts
+++ b/src/app/+browse-by/browse-by-routing.module.ts
@@ -1,13 +1,13 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
-import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
+import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'title', component: BrowseByTitlePageComponent },
- { path: 'author', component: BrowseByAuthorPageComponent }
+ { path: ':metadata', component: BrowseByMetadataPageComponent }
])
]
})
diff --git a/src/app/+browse-by/browse-by.module.ts b/src/app/+browse-by/browse-by.module.ts
index 51843a13d8..38e5001b80 100644
--- a/src/app/+browse-by/browse-by.module.ts
+++ b/src/app/+browse-by/browse-by.module.ts
@@ -4,8 +4,8 @@ import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-ti
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
-import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
+import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
@NgModule({
imports: [
@@ -15,7 +15,7 @@ import { BrowseService } from '../core/browse/browse.service';
],
declarations: [
BrowseByTitlePageComponent,
- BrowseByAuthorPageComponent
+ BrowseByMetadataPageComponent
],
providers: [
ItemDataService,
diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html
index a233163070..6e411cb29d 100644
--- a/src/app/+collection-page/collection-page.component.html
+++ b/src/app/+collection-page/collection-page.component.html
@@ -7,6 +7,8 @@
+
+
+
+
{
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, undefined);
});
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, undefined);
});
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);
});
});
});
diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts
index b807a77e99..e892024711 100644
--- a/src/app/core/browse/browse.service.ts
+++ b/src/app/core/browse/browse.service.ts
@@ -33,6 +33,7 @@ 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';
@Injectable()
export class BrowseService {
@@ -80,18 +81,18 @@ export class BrowseService {
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
- getBrowseEntriesFor(definitionID: string, options: {
- pagination?: PaginationComponentOptions;
- sort?: SortOptions;
- } = {}): Observable>> {
+ getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable>> {
const request$ = this.getBrowseDefinitions().pipe(
- getBrowseDefinitionLinks(definitionID),
+ getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(),
map((_links: any) => _links.entries),
hasValueOperator(),
map((href: string) => {
// TODO nearly identical to PaginatedSearchOptions => refactor
const args = [];
+ if (isNotEmpty(options.sort)) {
+ args.push(`scope=${options.scope}`);
+ }
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
@@ -133,17 +134,17 @@ export class BrowseService {
* sort: SortOptions }
* @returns {Observable>>}
*/
- getBrowseItemsFor(definitionID: string, filterValue: string, options: {
- pagination?: PaginationComponentOptions;
- sort?: SortOptions;
- } = {}): Observable>> {
+ getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable>> {
const request$ = this.getBrowseDefinitions().pipe(
- getBrowseDefinitionLinks(definitionID),
+ getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
+ if (isNotEmpty(options.sort)) {
+ args.push(`scope=${options.scope}`);
+ }
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
diff --git a/src/app/core/data/browse-entries-response-parsing.service.spec.ts b/src/app/core/data/browse-entries-response-parsing.service.spec.ts
index a3049cb061..ef9a833765 100644
--- a/src/app/core/data/browse-entries-response-parsing.service.spec.ts
+++ b/src/app/core/data/browse-entries-response-parsing.service.spec.ts
@@ -106,21 +106,6 @@ describe('BrowseEntriesResponseParsingService', () => {
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
- payload: {
- authority: null,
- value: 'Arulmozhiyal, Ramaswamy',
- valueLang: null,
- count: 1,
- type: 'browseEntry',
- _links: {
- self: {
- href: 'https://rest.api/discover/browses/author/entries'
- },
- items: {
- href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
- }
- },
- },
statusCode: 200,
statusText: 'OK'
} as DSpaceRESTV2Response;
diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts
index c661837da8..da2815f3d9 100644
--- a/src/app/core/data/browse-entries-response-parsing.service.ts
+++ b/src/app/core/data/browse-entries-response-parsing.service.ts
@@ -30,11 +30,13 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
- if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
- && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
- const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
- const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
- return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
+ if (isNotEmpty(data.payload)) {
+ let browseEntries = [];
+ if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
+ const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
+ browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
+ }
+ return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts
index 3e98078514..54933ac823 100644
--- a/src/app/core/data/dso-response-parsing.service.ts
+++ b/src/app/core/data/dso-response-parsing.service.ts
@@ -28,7 +28,13 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
- const processRequestDTO = this.process(data.payload, request.uuid);
+ let processRequestDTO;
+ // Prevent empty pages returning an error, initialize empty array instead.
+ if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) {
+ processRequestDTO = { page: [] };
+ } else {
+ processRequestDTO = this.process(data.payload, request.uuid);
+ }
let objectList = processRequestDTO;
if (hasNoValue(processRequestDTO)) {
diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts
index 54d409d751..df6226aedc 100644
--- a/src/app/navbar/navbar.component.ts
+++ b/src/app/navbar/navbar.component.ts
@@ -51,28 +51,18 @@ export class NavbarComponent extends MenuComponent implements OnInit {
} as TextMenuItemModel,
index: 0
},
+ // {
+ // id: 'browse_global_communities_and_collections',
+ // parentID: 'browse_global',
+ // active: false,
+ // visible: true,
+ // model: {
+ // type: MenuItemType.LINK,
+ // text: 'menu.section.browse_global_communities_and_collections',
+ // link: '#'
+ // } as LinkMenuItemModel,
+ // },
{
- id: 'browse_global_communities_and_collections',
- parentID: 'browse_global',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.browse_global_communities_and_collections',
- link: '#'
- } as LinkMenuItemModel,
- },
- {
- id: 'browse_global_global_by_issue_date',
- parentID: 'browse_global',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.browse_global_by_issue_date',
- link: '#'
- } as LinkMenuItemModel,
- }, {
id: 'browse_global_global_by_title',
parentID: 'browse_global',
active: false,
@@ -94,6 +84,17 @@ export class NavbarComponent extends MenuComponent implements OnInit {
link: '/browse/author'
} as LinkMenuItemModel,
},
+ {
+ id: 'browse_global_by_subject',
+ parentID: 'browse_global',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.browse_global_by_subject',
+ link: '/browse/subject'
+ } as LinkMenuItemModel,
+ },
/* Statistics */
{
diff --git a/src/app/shared/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html
index f30c5b905c..fc3300ae72 100644
--- a/src/app/shared/browse-by/browse-by.component.html
+++ b/src/app/shared/browse-by/browse-by.component.html
@@ -1,5 +1,5 @@
- {{title}}
+ {{title | translate}}
0" @fadeIn>
-
+
+
+ {{'browse.empty' | translate}}
+
diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts
index 94cf81f46e..2e9e825a6b 100644
--- a/src/app/shared/browse-by/browse-by.component.ts
+++ b/src/app/shared/browse-by/browse-by.component.ts
@@ -5,7 +5,6 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { Observable } from 'rxjs';
-import { Item } from '../../core/shared/item.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
@Component({
@@ -21,10 +20,23 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
* Component to display a browse-by page for any ListableObject
*/
export class BrowseByComponent {
+ /**
+ * The i18n message to display as title
+ */
@Input() title: string;
+
+ /**
+ * The list of objects to display
+ */
@Input() objects$: Observable>>;
+
+ /**
+ * The pagination configuration used for the list
+ */
@Input() paginationConfig: PaginationComponentOptions;
+
+ /**
+ * The sorting configuration used for the list
+ */
@Input() sortConfig: SortOptions;
- @Input() currentUrl: string;
- query: string;
}
diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html
new file mode 100644
index 0000000000..653bd1ed53
--- /dev/null
+++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html
@@ -0,0 +1,6 @@
+{{'browse.comcol.head' | translate}}
+
diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts
new file mode 100644
index 0000000000..85d40a77e0
--- /dev/null
+++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+
+/**
+ * A component to display the "Browse By" section of a Community or Collection page
+ * It expects the ID of the Community or Collection as input to be passed on as a scope
+ */
+@Component({
+ selector: 'ds-comcol-page-browse-by',
+ templateUrl: './comcol-page-browse-by.component.html',
+})
+export class ComcolPageBrowseByComponent {
+ /**
+ * The ID of the Community or Collection
+ */
+ @Input() id: string;
+}
diff --git a/src/app/shared/comcol-page-content/comcol-page-content.component.html b/src/app/shared/comcol-page-content/comcol-page-content.component.html
index 4a0be8cfc7..16b20cde20 100644
--- a/src/app/shared/comcol-page-content/comcol-page-content.component.html
+++ b/src/app/shared/comcol-page-content/comcol-page-content.component.html
@@ -1,5 +1,5 @@
-
+
{{ title | translate }}
{{content}}
-
\ No newline at end of file
+
diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html
index 198e79b453..6139e4a9df 100644
--- a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html
+++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html
@@ -1,5 +1,5 @@
-
+
{{object.value}}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 47cbcdabd3..b7250a6e18 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -94,6 +94,7 @@ import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/cre
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
+import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -143,6 +144,7 @@ const COMPONENTS = [
CreateComColPageComponent,
EditComColPageComponent,
DeleteComColPageComponent,
+ ComcolPageBrowseByComponent,
DsDynamicFormComponent,
DsDynamicFormControlContainerComponent,
DsDynamicListComponent,