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}}

- + +
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,