From 883a1d8881e43f2394d35193bcd787cfc8315f64 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 8 Aug 2018 11:36:42 +0200 Subject: [PATCH] Issues 252,253: Browse by title and browse by metadata (author) --- resources/i18n/en.json | 7 +- .../browse-by-author-page.component.html | 11 ++ .../browse-by-author-page.component.scss | 0 .../browse-by-author-page.component.spec.ts | 0 .../browse-by-author-page.component.ts | 107 +++++++++++ .../browse-by-title-page.component.html | 11 ++ .../browse-by-title-page.component.scss | 0 .../browse-by-title-page.component.spec.ts | 0 .../browse-by-title-page.component.ts | 85 +++++++++ .../+browse-by/browse-by-routing.module.ts | 16 ++ src/app/+browse-by/browse-by.module.ts | 27 +++ src/app/app-routing.module.ts | 1 + src/app/core/browse/browse.service.ts | 77 ++++++-- src/app/core/core.module.ts | 2 + ...wse-items-response-parsing-service.spec.ts | 168 ++++++++++++++++++ .../browse-items-response-parsing-service.ts | 49 +++++ src/app/core/data/comcol-data.service.spec.ts | 16 +- src/app/core/data/comcol-data.service.ts | 14 +- src/app/core/data/data.service.ts | 13 +- src/app/core/data/item-data.service.spec.ts | 21 ++- src/app/core/data/item-data.service.ts | 17 +- src/app/core/data/request.models.ts | 8 + src/app/core/shared/browse-entry.model.ts | 3 +- src/app/core/shared/dspace-object.model.ts | 5 + src/app/core/shared/operators.ts | 19 +- .../shared/browse-by/browse-by.component.html | 12 ++ .../shared/browse-by/browse-by.component.scss | 0 .../browse-by/browse-by.component.spec.ts | 44 +++++ .../shared/browse-by/browse-by.component.ts | 27 +++ .../object-collection.component.html | 2 +- .../item-grid-element.component.html | 40 +++-- .../browse-entry-list-element.component.html | 3 + .../browse-entry-list-element.component.scss | 1 + ...rowse-entry-list-element.component.spec.ts | 47 +++++ .../browse-entry-list-element.component.ts | 17 ++ .../item-list-element.component.html | 40 +++-- .../metadata-list-element.component.html | 3 + .../metadata-list-element.component.scss | 1 + .../metadata-list-element.component.spec.ts | 48 +++++ .../metadata-list-element.component.ts | 15 ++ src/app/shared/shared.module.ts | 7 + 41 files changed, 898 insertions(+), 86 deletions(-) create mode 100644 src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.html create mode 100644 src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.scss create mode 100644 src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.spec.ts create mode 100644 src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts create mode 100644 src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html create mode 100644 src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.scss create mode 100644 src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts create mode 100644 src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts create mode 100644 src/app/+browse-by/browse-by-routing.module.ts create mode 100644 src/app/+browse-by/browse-by.module.ts create mode 100644 src/app/core/data/browse-items-response-parsing-service.spec.ts create mode 100644 src/app/core/data/browse-items-response-parsing-service.ts create mode 100644 src/app/shared/browse-by/browse-by.component.html create mode 100644 src/app/shared/browse-by/browse-by.component.scss create mode 100644 src/app/shared/browse-by/browse-by.component.spec.ts create mode 100644 src/app/shared/browse-by/browse-by.component.ts create mode 100644 src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html create mode 100644 src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.scss create mode 100644 src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts create mode 100644 src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts create mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html create mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss create mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts create mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ba70b87e12..f08213df06 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -135,6 +135,9 @@ } } }, + "browse": { + "title": "Browsing {{ collection }} by {{ field }} {{ value }}" + }, "admin": { "registries": { "metadata": { @@ -193,7 +196,8 @@ "recent-submissions": "Loading recent submissions...", "item": "Loading item...", "objects": "Loading...", - "search-results": "Loading search results..." + "search-results": "Loading search results...", + "browse-by": "Loading items..." }, "error": { "default": "Error", @@ -205,6 +209,7 @@ "item": "Error fetching item", "objects": "Error fetching objects", "search-results": "Error fetching search results", + "browse-by": "Error fetching items", "validation": { "pattern": "This input is restricted by the current pattern: {{ pattern }}.", "license": { 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 new file mode 100644 index 0000000000..438c318994 --- /dev/null +++ b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.html @@ -0,0 +1,11 @@ +
+
+ + +
+
diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.scss b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.spec.ts b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..1605b4afab --- /dev/null +++ b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts @@ -0,0 +1,107 @@ +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 { Observable } from 'rxjs/Observable'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { Subscription } from 'rxjs/Subscription'; +import { ActivatedRoute } from '@angular/router'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { Metadatum } from '../../core/shared/metadatum.model'; +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' +}) +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( + Observable.combineLatest( + 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; + const startsWith = +params.query || params.query || ''; + 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, + startsWith: startsWith + }; + if (isNotEmpty(this.value)) { + this.updatePageWithItems(searchOptions, this.value); + } else { + this.updatePage(searchOptions); + } + })); + } + + /** + * @param searchOptions Options to narrow down your search: + * { pagination: PaginationComponentOptions, + * sort: SortOptions, + * startsWith: string } + */ + updatePage(searchOptions) { + this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions); + this.items$ = undefined; + } + + /** + * @param searchOptions Options to narrow down your search: + * { pagination: PaginationComponentOptions, + * sort: SortOptions, + * startsWith: string } + * @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-title-page/browse-by-title-page.component.html b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html new file mode 100644 index 0000000000..d37727be36 --- /dev/null +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.html @@ -0,0 +1,11 @@ +
+
+ + +
+
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.scss b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 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..e69de29bb2 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 new file mode 100644 index 0000000000..6483ac8d17 --- /dev/null +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts @@ -0,0 +1,85 @@ +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 { Observable } from 'rxjs/Observable'; +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 { Subscription } from 'rxjs/Subscription'; +import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router'; +import { hasValue } from '../../shared/empty.util'; +import { Collection } from '../../core/shared/collection.model'; + +@Component({ + selector: 'ds-browse-by-title-page', + styleUrls: ['./browse-by-title-page.component.scss'], + templateUrl: './browse-by-title-page.component.html' +}) +export class BrowseByTitlePageComponent implements OnInit { + + items$: Observable>>; + 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) { + + } + + 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( + Observable.combineLatest( + 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.page || this.sortConfig.direction; + const startsWith = +params.query || params.query || ''; + const pagination = Object.assign({}, + this.paginationConfig, + { currentPage: page, pageSize: pageSize } + ); + const sort = Object.assign({}, + this.sortConfig, + { direction: sortDirection, field: params.sortField } + ); + this.updatePage({ + pagination: pagination, + sort: sort, + startsWith: startsWith + }); + })); + } + + updatePage(searchOptions) { + this.items$ = this.itemDataService.findAll({ + currentPage: searchOptions.pagination.currentPage, + elementsPerPage: searchOptions.pagination.pageSize, + sort: searchOptions.sort, + startsWith: searchOptions.startsWith + }); + } + + 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 new file mode 100644 index 0000000000..630a7c0db5 --- /dev/null +++ b/src/app/+browse-by/browse-by-routing.module.ts @@ -0,0 +1,16 @@ +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'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'title', component: BrowseByTitlePageComponent }, + { path: 'author', component: BrowseByAuthorPageComponent } + ]) + ] +}) +export class BrowseByRoutingModule { + +} diff --git a/src/app/+browse-by/browse-by.module.ts b/src/app/+browse-by/browse-by.module.ts new file mode 100644 index 0000000000..51843a13d8 --- /dev/null +++ b/src/app/+browse-by/browse-by.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component'; +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'; + +@NgModule({ + imports: [ + BrowseByRoutingModule, + CommonModule, + SharedModule + ], + declarations: [ + BrowseByTitlePageComponent, + BrowseByAuthorPageComponent + ], + providers: [ + ItemDataService, + BrowseService + ] +}) +export class BrowseByModule { + +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4bc8c43152..7de83651ff 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; { path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, + { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' }, { path: 'admin', loadChildren: './+admin/admin.module#AdminModule' }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 836014a110..2e4a0a3508 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -16,19 +16,27 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models'; +import { + BrowseEndpointRequest, + BrowseEntriesRequest, + BrowseItemsRequest, + GetRequest, + RestRequest +} from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { configureRequest, - filterSuccessfulResponses, + filterSuccessfulResponses, getBrowseDefinitionLinks, getRemoteDataPayload, getRequestFromSelflink, getResponseFromSelflink } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { Item } from '../shared/item.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class BrowseService { @@ -71,6 +79,8 @@ export class BrowseService { map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => response.payload), ensureArrayHasValue(), + map((definitions: BrowseDefinition[]) => definitions + .map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))), distinctUntilChanged() ); @@ -82,17 +92,7 @@ export class BrowseService { sort?: SortOptions; } = {}): Observable>> { const request$ = this.getBrowseDefinitions().pipe( - getRemoteDataPayload(), - map((browseDefinitions: BrowseDefinition[]) => browseDefinitions - .find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true) - ), - map((def: BrowseDefinition) => { - if (isNotEmpty(def)) { - return def._links; - } else { - throw new Error(`No metadata browse definition could be found for id '${definitionID}'`); - } - }), + getBrowseDefinitionLinks(definitionID), hasValueOperator(), map((_links: any) => _links.entries), hasValueOperator(), @@ -124,6 +124,57 @@ export class BrowseService { filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), + map((list: PaginatedList) => Object.assign(list, { + page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page + })), + distinctUntilChanged() + ); + + return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + } + + getBrowseItemsFor(definitionID: string, filterValue: string, options: { + pagination?: PaginationComponentOptions; + sort?: SortOptions; + } = {}): Observable>> { + const request$ = this.getBrowseDefinitions().pipe( + getBrowseDefinitionLinks(definitionID), + hasValueOperator(), + map((_links: any) => _links.items), + hasValueOperator(), + map((href: string) => { + const args = []; + if (isNotEmpty(options.sort)) { + args.push(`sort=${options.sort.field},${options.sort.direction}`); + } + if (isNotEmpty(options.pagination)) { + args.push(`page=${options.pagination.currentPage - 1}`); + args.push(`size=${options.pagination.pageSize}`); + } + if (isNotEmpty(filterValue)) { + args.push(`filterValue=${filterValue}`); + } + if (isNotEmpty(args)) { + href = new URLCombiner(href, `?${args.join('&')}`).toString(); + } + return href; + }), + map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService) + ); + + const href$ = request$.pipe(map((request: RestRequest) => request.href)); + + const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); + + const payload$ = responseCache$.pipe( + filterSuccessfulResponses(), + map((entry: ResponseCacheEntry) => entry.response), + map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), + map((list: PaginatedList) => Object.assign(list, { + page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page + })), distinctUntilChanged() ); diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8536169688..422717e233 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -62,6 +62,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { UploaderService } from '../shared/uploader/uploader.service'; +import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; const IMPORTS = [ CommonModule, @@ -114,6 +115,7 @@ const PROVIDERS = [ ServerResponseService, BrowseResponseParsingService, BrowseEntriesResponseParsingService, + BrowseItemsResponseParsingService, BrowseService, ConfigResponseParsingService, RouteService, diff --git a/src/app/core/data/browse-items-response-parsing-service.spec.ts b/src/app/core/data/browse-items-response-parsing-service.spec.ts new file mode 100644 index 0000000000..6a141c01c4 --- /dev/null +++ b/src/app/core/data/browse-items-response-parsing-service.spec.ts @@ -0,0 +1,168 @@ +import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; +import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; +import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models'; +import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; + +describe('BrowseItemsResponseParsingService', () => { + let service: BrowseItemsResponseParsingService; + + beforeEach(() => { + service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService()); + }); + + describe('parse', () => { + const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items'); + + const validResponse = { + payload: { + _embedded: { + items: [ + { + id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', + uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', + name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India', + handle: '10986/17472', + metadata: [ + { + key: 'dc.creator', + value: 'World Bank', + language: null + } + ], + inArchive: true, + discoverable: true, + withdrawn: false, + lastModified: '2018-05-25T09:32:58.005+0000', + type: 'item', + _links: { + bitstreams: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams' + }, + owningCollection: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection' + }, + templateItemOf: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf' + }, + self: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7' + } + } + }, + { + id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b', + uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b', + name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India', + handle: '10986/17475', + metadata: [ + { + key: 'dc.creator', + value: 'World Bank', + language: null + } + ], + inArchive: true, + discoverable: true, + withdrawn: false, + lastModified: '2018-05-25T09:33:42.526+0000', + type: 'item', + _links: { + bitstreams: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams' + }, + owningCollection: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection' + }, + templateItemOf: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf' + }, + self: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b' + } + } + } + ] + }, + _links: { + first: { + href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2' + }, + self: { + href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items' + }, + next: { + href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2' + }, + last: { + href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2' + } + }, + page: { + size: 2, + totalElements: 16, + totalPages: 8, + number: 0 + } + }, + statusCode: '200' + } as DSpaceRESTV2Response; + + const invalidResponseNotAList = { + payload: { + id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', + uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7', + name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India', + handle: '10986/17472', + metadata: [ + { + key: 'dc.creator', + value: 'World Bank', + language: null + } + ], + inArchive: true, + discoverable: true, + withdrawn: false, + lastModified: '2018-05-25T09:32:58.005+0000', + type: 'item', + _links: { + bitstreams: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams' + }, + owningCollection: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection' + }, + templateItemOf: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf' + }, + self: { + href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7' + } + } + }, + statusCode: '200' + } as DSpaceRESTV2Response; + + const invalidResponseStatusCode = { + payload: {}, statusCode: '500' + } as DSpaceRESTV2Response; + + it('should return a GenericSuccessResponse if data contains a valid browse items response', () => { + const response = service.parse(request, validResponse); + expect(response.constructor).toBe(GenericSuccessResponse); + }); + + it('should return an ErrorResponse if data contains an invalid browse entries response', () => { + const response = service.parse(request, invalidResponseNotAList); + expect(response.constructor).toBe(ErrorResponse); + }); + + it('should return an ErrorResponse if data contains a statuscode other than 200', () => { + const response = service.parse(request, invalidResponseStatusCode); + expect(response.constructor).toBe(ErrorResponse); + }); + + }); +}); diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts new file mode 100644 index 0000000000..840d59b863 --- /dev/null +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable } from '@angular/core'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { isNotEmpty } from '../../shared/empty.util'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { + ErrorResponse, + GenericSuccessResponse, + RestResponse +} from '../cache/response-cache.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { Item } from '../shared/item.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; + +@Injectable() +export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = { + getConstructor: () => DSpaceObject + }; + protected toCache = false; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + + 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(DSpaceObject); + const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload)); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from browse endpoint'), + { statusText: data.statusCode } + ) + ); + } + } + +} diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5727fb22f..59ae6619d8 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -10,7 +10,7 @@ import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; -import { FindByIDRequest } from './request.models'; +import { FindAllOptions, FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -53,6 +53,10 @@ describe('ComColDataService', () => { const EnvConfig = {} as GlobalConfig; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; + const options = Object.assign(new FindAllOptions(), { + scopeID: scopeID + }); + const communitiesEndpoint = 'https://rest.api/core/communities'; const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; @@ -99,7 +103,7 @@ describe('ComColDataService', () => { ); } - describe('getScopedEndpoint', () => { + describe('getBrowseEndpoint', () => { beforeEach(() => { scheduler = getTestScheduler(); }); @@ -113,7 +117,7 @@ describe('ComColDataService', () => { const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); - scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); + scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); @@ -129,13 +133,13 @@ describe('ComColDataService', () => { }); it('should fetch the scope Community from the cache', () => { - scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); + scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.flush(); expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID); }); it('should return the endpoint to fetch resources within the given scope', () => { - const result = service.getScopedEndpoint(scopeID); + const result = service.getBrowseEndpoint(options); const expected = cold('--e-', { e: scopedEndpoint }); expect(result).toBeObservable(expected); @@ -152,7 +156,7 @@ describe('ComColDataService', () => { }); it('should throw an error', () => { - const result = service.getScopedEndpoint(scopeID); + const result = service.getBrowseEndpoint(options); const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`)); expect(result).toBeObservable(expected); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 112afa0bc8..bb624eda0f 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -8,7 +8,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; -import { FindByIDRequest } from './request.models'; +import { FindAllOptions, FindByIDRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -27,16 +27,16 @@ export abstract class ComColDataService } * an Observable containing the scoped URL */ - public getScopedEndpoint(scopeID: string): Observable { - if (isEmpty(scopeID)) { + public getBrowseEndpoint(options: FindAllOptions = {}): Observable { + if (isEmpty(options.scopeID)) { return this.halService.getEndpoint(this.linkPath); } else { const scopeCommunityHrefObs = this.cds.getEndpoint() - .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) + .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)) .filter((href: string) => isNotEmpty(href)) .take(1) .do((href: string) => { - const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID); + const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); this.requestService.configure(request); }); @@ -48,9 +48,9 @@ export abstract class ComColDataService - Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), + Observable.throw(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))), successResponse - .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID)) + .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(options.scopeID)) .map((nc: NormalizedCommunity) => nc._links[this.linkPath]) .filter((href) => isNotEmpty(href)) ).distinctUntilChanged(); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index f532ff05ba..5c01412ba7 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -20,17 +20,13 @@ export abstract class DataService protected abstract linkPath: string; protected abstract halService: HALEndpointService; - public abstract getScopedEndpoint(scope: string): Observable + public abstract getBrowseEndpoint(options: FindAllOptions): Observable - protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable { + protected getFindAllHref(options: FindAllOptions = {}): Observable { let result: Observable; const args = []; - if (hasValue(options.scopeID)) { - result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged(); - } else { - result = Observable.of(endpoint); - } + result = this.getBrowseEndpoint(options).distinctUntilChanged(); if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ @@ -53,8 +49,7 @@ export abstract class DataService } findAll(options: FindAllOptions = {}): Observable>> { - const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href)) - .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); + const hrefObs = this.getFindAllHref(options); hrefObs .filter((href: string) => hasValue(href)) diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 4d0dc8aec3..718fbde2f6 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { FindAllOptions } from './request.models'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -20,9 +21,19 @@ describe('ItemDataService', () => { const halEndpointService = {} as HALEndpointService; const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; + const startsWith = 'a'; + const options = Object.assign(new FindAllOptions(), { + scopeID: scopeID, + sort: { + field: '', + direction: undefined + }, + startsWith: startsWith + }); + const browsesEndpoint = 'https://rest.api/discover/browses'; const itemBrowseEndpoint = `${browsesEndpoint}/author/items`; - const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; + const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}&startsWith=${startsWith}`; const serviceEndpoint = `https://rest.api/core/items`; const browseError = new Error('getBrowseURL failed'); @@ -46,16 +57,16 @@ describe('ItemDataService', () => { ); } - describe('getScopedEndpoint', () => { + describe('getBrowseEndpoint', () => { beforeEach(() => { scheduler = getTestScheduler(); }); - it('should return the endpoint to fetch Items within the given scope', () => { + it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => { bs = initMockBrowseService(true); service = initTestService(); - const result = service.getScopedEndpoint(scopeID); + const result = service.getBrowseEndpoint(options); const expected = cold('--b-', { b: scopedEndpoint }); expect(result).toBeObservable(expected); @@ -67,7 +78,7 @@ describe('ItemDataService', () => { service = initTestService(); }); it('should throw an error', () => { - const result = service.getScopedEndpoint(scopeID); + const result = service.getBrowseEndpoint(options); const expected = cold('--#-', undefined, browseError); expect(result).toBeObservable(expected); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 6b0937d8e4..eb0896efdd 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -15,6 +15,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { FindAllOptions } from './request.models'; @Injectable() export class ItemDataService extends DataService { @@ -30,15 +31,15 @@ export class ItemDataService extends DataService { super(); } - public getScopedEndpoint(scopeID: string): Observable { - if (isEmpty(scopeID)) { - return this.halService.getEndpoint(this.linkPath); - } else { - return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath) - .filter((href: string) => isNotEmpty(href)) - .map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()) - .distinctUntilChanged(); + public getBrowseEndpoint(options: FindAllOptions = {}): Observable { + let field = 'dc.date.issued'; + if (options.sort && options.sort.field) { + field = options.sort.field; } + return this.bs.getBrowseURLFor(field, this.linkPath) + .filter((href: string) => isNotEmpty(href)) + .map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}` + (options.startsWith ? `&startsWith=${options.startsWith}` : '')).toString()) + .distinctUntilChanged(); } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 7015b0b0f1..b87f9cefc8 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -12,6 +12,7 @@ import { AuthResponseParsingService } from '../auth/auth-response-parsing.servic import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; +import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; /* tslint:disable:max-classes-per-file */ @@ -141,6 +142,7 @@ export class FindAllOptions { elementsPerPage?: number; currentPage?: number; sort?: SortOptions; + startsWith?: string; } export class FindAllRequest extends GetRequest { @@ -183,6 +185,12 @@ export class BrowseEntriesRequest extends GetRequest { } } +export class BrowseItemsRequest extends GetRequest { + getResponseParser(): GenericConstructor { + return BrowseItemsResponseParsingService; + } +} + export class ConfigRequest extends GetRequest { constructor(uuid: string, href: string) { super(uuid, href); diff --git a/src/app/core/shared/browse-entry.model.ts b/src/app/core/shared/browse-entry.model.ts index fede195a39..932c6946d1 100644 --- a/src/app/core/shared/browse-entry.model.ts +++ b/src/app/core/shared/browse-entry.model.ts @@ -1,6 +1,7 @@ import { autoserialize, autoserializeAs } from 'cerialize'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -export class BrowseEntry { +export class BrowseEntry implements ListableObject { @autoserialize type: string; diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 5e62e3e321..042bfc01a0 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -5,6 +5,7 @@ import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { Observable } from 'rxjs/Observable'; +import { autoserialize } from 'cerialize'; /** * An abstract model class for a DSpaceObject. @@ -16,11 +17,13 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * The human-readable identifier of this DSpaceObject */ + @autoserialize id: string; /** * The universally unique identifier of this DSpaceObject */ + @autoserialize uuid: string; /** @@ -31,11 +34,13 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * The name for this DSpaceObject */ + @autoserialize name: string; /** * An array containing all metadata of this DSpaceObject */ + @autoserialize metadata: Metadatum[]; /** diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index c0b9be3fbf..9009473765 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs/Observable'; import { filter, flatMap, map, tap } from 'rxjs/operators'; -import { hasValueOperator } from '../../shared/empty.util'; +import { hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse } from '../cache/response-cache.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; @@ -8,6 +8,7 @@ import { RemoteData } from '../data/remote-data'; import { RestRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; +import { BrowseDefinition } from './browse-definition.model'; /** * This file contains custom RxJS operators that can be used in multiple places @@ -45,3 +46,19 @@ export const configureRequest = (requestService: RequestService) => export const getRemoteDataPayload = () => (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); + +export const getBrowseDefinitionLinks = (definitionID: string) => + (source: Observable>): Observable => + source.pipe( + getRemoteDataPayload(), + map((browseDefinitions: BrowseDefinition[]) => browseDefinitions + .find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true) + ), + map((def: BrowseDefinition) => { + if (isNotEmpty(def)) { + return def._links; + } else { + throw new Error(`No metadata browse definition could be found for id '${definitionID}'`); + } + }) + ); diff --git a/src/app/shared/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html new file mode 100644 index 0000000000..f30c5b905c --- /dev/null +++ b/src/app/shared/browse-by/browse-by.component.html @@ -0,0 +1,12 @@ + +

{{title}}

+
+ + +
+ + +
diff --git a/src/app/shared/browse-by/browse-by.component.scss b/src/app/shared/browse-by/browse-by.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts new file mode 100644 index 0000000000..883d61a221 --- /dev/null +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -0,0 +1,44 @@ +import { BrowseByComponent } from './browse-by.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { SharedModule } from '../shared.module'; + +describe('BrowseByComponent', () => { + let comp: BrowseByComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule], + declarations: [], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BrowseByComponent); + comp = fixture.componentInstance; + }); + + it('should display a loading message when objects is empty',() => { + (comp as any).objects = undefined; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('ds-loading'))).toBeDefined(); + }); + + it('should display results when objects is not empty', () => { + (comp as any).objects = Observable.of({ + payload: { + page: { + length: 1 + } + } + }); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined(); + }); + +}); diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts new file mode 100644 index 0000000000..196134224c --- /dev/null +++ b/src/app/shared/browse-by/browse-by.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { fadeIn, fadeInOut } from '../animations/fade'; +import { Observable } from 'rxjs/Observable'; +import { Item } from '../../core/shared/item.model'; +import { ListableObject } from '../object-collection/shared/listable-object.model'; + +@Component({ + selector: 'ds-browse-by', + styleUrls: ['./browse-by.component.scss'], + templateUrl: './browse-by.component.html', + animations: [ + fadeIn, + fadeInOut + ] +}) +export class BrowseByComponent { + @Input() title: string; + @Input() objects$: Observable>>; + @Input() paginationConfig: PaginationComponentOptions; + @Input() sortConfig: SortOptions; + @Input() currentUrl: string; + query: string; +} diff --git a/src/app/shared/object-collection/object-collection.component.html b/src/app/shared/object-collection/object-collection.component.html index c9b1cb92f5..b1d07db876 100644 --- a/src/app/shared/object-collection/object-collection.component.html +++ b/src/app/shared/object-collection/object-collection.component.html @@ -8,7 +8,7 @@ diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html index cc2f2efdb1..ee1b9c4239 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html @@ -1,22 +1,28 @@ -
+ +
+ + + + +
+

{{object.findMetadata('dc.title')}}

- - - - -
-

{{object.findMetadata('dc.title')}}

-

- {{authorMd.value}} - ; - - {{object.findMetadata("dc.date.issued")}} -

+ +

+ {{authorMd.value}} + ; + + {{object.findMetadata("dc.date.issued")}} +

+
-

{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}

+ +

{{object.findMetadata("dc.description.abstract") }}

+
-
- View +
+ View +
-
+ 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 new file mode 100644 index 0000000000..9eedb70eab --- /dev/null +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html @@ -0,0 +1,3 @@ + + {{object.value}} + diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.scss b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.scss new file mode 100644 index 0000000000..45a533cd01 --- /dev/null +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/variables'; diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts new file mode 100644 index 0000000000..de53f2e095 --- /dev/null +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts @@ -0,0 +1,47 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { Metadatum } from '../../../core/shared/metadatum.model'; +import { BrowseEntryListElementComponent } from './browse-entry-list-element.component'; +import { BrowseEntry } from '../../../core/shared/browse-entry.model'; + +let browseEntryListElementComponent: BrowseEntryListElementComponent; +let fixture: ComponentFixture; + +const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), { + type: 'browseEntry', + value: 'De Langhe Kristof' +}); + +describe('MetadataListElementComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BrowseEntryListElementComponent , TruncatePipe], + providers: [ + { provide: 'objectElementProvider', useValue: {mockValue}} + ], + + schemas: [ NO_ERRORS_SCHEMA ] + }).overrideComponent(BrowseEntryListElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(BrowseEntryListElementComponent); + browseEntryListElementComponent = fixture.componentInstance; + })); + + describe('When the metadatum is loaded', () => { + beforeEach(() => { + browseEntryListElementComponent.object = mockValue; + fixture.detectChanges(); + }); + + it('should show the value as a link', () => { + const browseEntryLink = fixture.debugElement.query(By.css('a.lead')); + expect(browseEntryLink.nativeElement.textContent.trim()).toBe(mockValue.value); + }); + }); +}); diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts new file mode 100644 index 0000000000..a5c92e87ae --- /dev/null +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, Inject } from '@angular/core'; + +import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; +import { ViewMode } from '../../../+search-page/search-options.model'; +import { BrowseEntry } from '../../../core/shared/browse-entry.model'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ListableObject } from '../../object-collection/shared/listable-object.model'; + +@Component({ + selector: 'ds-browse-entry-list-element', + styleUrls: ['./browse-entry-list-element.component.scss'], + templateUrl: './browse-entry-list-element.component.html' +}) + +@renderElementsFor(BrowseEntry, ViewMode.List) +export class BrowseEntryListElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.html b/src/app/shared/object-list/item-list-element/item-list-element.component.html index b4259c25c2..28b83b4000 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.html @@ -1,18 +1,24 @@ - - {{object.findMetadata("dc.title")}} - -
- - - {{authorMd.value}} - ; + + + {{object.findMetadata("dc.title")}} + +
+ + + + {{authorMd.value}} + ; + - - ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) - -
- {{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }} -
-
+ ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) +
+ + +
+ {{object.findMetadata("dc.description.abstract")}} +
+
+
+ diff --git a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html new file mode 100644 index 0000000000..c05c4c10c9 --- /dev/null +++ b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html @@ -0,0 +1,3 @@ + + {{object.value}} + diff --git a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss new file mode 100644 index 0000000000..45a533cd01 --- /dev/null +++ b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/variables'; diff --git a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..8098a6a8c9 --- /dev/null +++ b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts @@ -0,0 +1,48 @@ +import { MetadataListElementComponent } from './metadata-list-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { Item } from '../../../core/shared/item.model'; +import { Observable } from 'rxjs/Observable'; +import { Metadatum } from '../../../core/shared/metadatum.model'; + +let metadataListElementComponent: MetadataListElementComponent; +let fixture: ComponentFixture; + +const mockValue: Metadatum = Object.assign(new Metadatum(), { + key: 'dc.contributor.author', + value: 'De Langhe Kristof' +}); + +describe('MetadataListElementComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MetadataListElementComponent , TruncatePipe], + providers: [ + { provide: 'objectElementProvider', useValue: {mockValue}} + ], + + schemas: [ NO_ERRORS_SCHEMA ] + }).overrideComponent(MetadataListElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(MetadataListElementComponent); + metadataListElementComponent = fixture.componentInstance; + })); + + describe('When the metadatum is loaded', () => { + beforeEach(() => { + metadataListElementComponent.object = mockValue; + fixture.detectChanges(); + }); + + it('should show the value as a link', () => { + const metadatumLink = fixture.debugElement.query(By.css('a.lead')); + expect(metadatumLink.nativeElement.textContent.trim()).toBe(mockValue.value); + }); + }); +}); diff --git a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts new file mode 100644 index 0000000000..12e140c7bf --- /dev/null +++ b/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts @@ -0,0 +1,15 @@ +import { Component, Input, Inject } from '@angular/core'; + +import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; +import { ViewMode } from '../../../+search-page/search-options.model'; +import { Metadatum } from '../../../core/shared/metadatum.model'; + +@Component({ + selector: 'ds-metadata-list-element', + styleUrls: ['./metadata-list-element.component.scss'], + templateUrl: './metadata-list-element.component.html' +}) + +@renderElementsFor(Metadatum, ViewMode.List) +export class MetadataListElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 57ba7dec4d..223eab4c69 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -72,6 +72,10 @@ import { NumberPickerComponent } from './number-picker/number-picker.component'; import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; import { MockAdminGuard } from './mocks/mock-admin-guard.service'; +import { MetadataListElementComponent } from './object-list/metadata-list-element/metadata-list-element.component'; +import { BrowseByModule } from '../+browse-by/browse-by.module'; +import { BrowseByComponent } from './browse-by/browse-by.component'; +import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -139,11 +143,13 @@ const COMPONENTS = [ ViewModeSwitchComponent, TruncatableComponent, TruncatablePartComponent, + BrowseByComponent ]; const ENTRY_COMPONENTS = [ // put shared entry components (components that are created dynamically) here ItemListElementComponent, + MetadataListElementComponent, CollectionListElementComponent, CommunityListElementComponent, SearchResultListElementComponent, @@ -151,6 +157,7 @@ const ENTRY_COMPONENTS = [ CollectionGridElementComponent, CommunityGridElementComponent, SearchResultGridElementComponent, + BrowseEntryListElementComponent ]; const PROVIDERS = [