From 883a1d8881e43f2394d35193bcd787cfc8315f64 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 8 Aug 2018 11:36:42 +0200 Subject: [PATCH 01/22] 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 = [ From 072507b2932472f3dc8c41fe878d68f282450123 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 9 Aug 2018 12:37:09 +0200 Subject: [PATCH 02/22] improved coverage, type docs, removed startsWith option and general refactoring --- .../browse-by-author-page.component.ts | 15 +++--- .../browse-by-title-page.component.ts | 16 +++++-- src/app/core/browse/browse.service.spec.ts | 39 +++++++++++++-- src/app/core/browse/browse.service.ts | 9 ++++ src/app/core/data/item-data.service.spec.ts | 6 +-- src/app/core/data/item-data.service.ts | 2 +- src/app/core/shared/operators.ts | 5 ++ .../shared/browse-by/browse-by.component.ts | 3 ++ .../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 | 2 - 13 files changed, 74 insertions(+), 90 deletions(-) delete mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html delete mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss delete mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts delete mode 100644 src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts 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 index 1605b4afab..1553889741 100644 --- 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 @@ -18,6 +18,9 @@ import { Item } from '../../core/shared/item.model'; 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>>; @@ -55,7 +58,6 @@ export class BrowseByAuthorPageComponent implements OnInit { 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, @@ -67,8 +69,7 @@ export class BrowseByAuthorPageComponent implements OnInit { ); const searchOptions = { pagination: pagination, - sort: sort, - startsWith: startsWith + sort: sort }; if (isNotEmpty(this.value)) { this.updatePageWithItems(searchOptions, this.value); @@ -79,10 +80,10 @@ export class BrowseByAuthorPageComponent implements OnInit { } /** + * Updates the current page with searchOptions * @param searchOptions Options to narrow down your search: * { pagination: PaginationComponentOptions, - * sort: SortOptions, - * startsWith: string } + * sort: SortOptions } */ updatePage(searchOptions) { this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions); @@ -90,10 +91,10 @@ export class BrowseByAuthorPageComponent implements OnInit { } /** + * Updates the current page with searchOptions and display items linked to author * @param searchOptions Options to narrow down your search: * { pagination: PaginationComponentOptions, - * sort: SortOptions, - * startsWith: string } + * sort: SortOptions } * @param author The author's name for displaying items */ updatePageWithItems(searchOptions, author: string) { 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 6483ac8d17..3205091952 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 @@ -17,6 +17,9 @@ import { Collection } from '../../core/shared/collection.model'; styleUrls: ['./browse-by-title-page.component.scss'], templateUrl: './browse-by-title-page.component.html' }) +/** + * Component for browsing items by title (dc.title) + */ export class BrowseByTitlePageComponent implements OnInit { items$: Observable>>; @@ -52,7 +55,6 @@ export class BrowseByTitlePageComponent implements OnInit { 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 } @@ -63,18 +65,22 @@ export class BrowseByTitlePageComponent implements OnInit { ); this.updatePage({ pagination: pagination, - sort: sort, - startsWith: startsWith + sort: sort }); })); } + /** + * Updates the current page with searchOptions + * @param searchOptions Options to narrow down your search: + * { pagination: PaginationComponentOptions, + * sort: SortOptions } + */ updatePage(searchOptions) { this.items$ = this.itemDataService.findAll({ currentPage: searchOptions.pagination.currentPage, elementsPerPage: searchOptions.pagination.pageSize, - sort: searchOptions.sort, - startsWith: searchOptions.startsWith + sort: searchOptions.sort }); } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 5118ea7ecc..daaca1e5b2 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -6,7 +6,7 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models'; +import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseService } from './browse.service'; @@ -143,7 +143,9 @@ describe('BrowseService', () => { }); - describe('getBrowseEntriesFor', () => { + describe('getBrowseEntriesFor and getBrowseItemsFor', () => { + const mockAuthorName = 'Donald Smith'; + beforeEach(() => { responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); @@ -156,7 +158,7 @@ describe('BrowseService', () => { spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); }); - describe('when called with a valid browse definition id', () => { + describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { it('should configure a new BrowseEntriesRequest', () => { const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries); @@ -175,7 +177,26 @@ describe('BrowseService', () => { }); - describe('when called with an invalid browse definition id', () => { + describe('when getBrowseItemsFor is called with a valid browse definition id', () => { + 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.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + + }); + + describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => { it('should throw an Error', () => { const definitionID = 'invalidID'; @@ -184,6 +205,16 @@ describe('BrowseService', () => { expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected); }); }); + + describe('when getBrowseItemsFor is called with an invalid browse definition id', () => { + it('should throw an Error', () => { + + 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); + }); + }); }); describe('getBrowseURLFor', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 2e4a0a3508..003f92698c 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -133,6 +133,15 @@ export class BrowseService { return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } + /** + * Get all items linked to a certain metadata value + * @param {string} definitionID definition ID to define the metadata-field (e.g. author) + * @param {string} filterValue metadata value to filter by (e.g. author's name) + * @param options Options to narrow down your search: + * { pagination: PaginationComponentOptions, + * sort: SortOptions } + * @returns {Observable>>} + */ getBrowseItemsFor(definitionID: string, filterValue: string, options: { pagination?: PaginationComponentOptions; sort?: SortOptions; diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 718fbde2f6..4cf126157a 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -21,19 +21,17 @@ 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}&startsWith=${startsWith}`; + const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; const serviceEndpoint = `https://rest.api/core/items`; const browseError = new Error('getBrowseURL failed'); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index eb0896efdd..e86003f308 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -38,7 +38,7 @@ export class ItemDataService extends DataService { } 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()) + .map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()) .distinctUntilChanged(); } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 9009473765..1efc7885c8 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -47,6 +47,11 @@ export const getRemoteDataPayload = () => (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); +/** + * Get the browse links from a definition by ID given an array of all definitions + * @param {string} definitionID + * @returns {(source: Observable>) => Observable} + */ export const getBrowseDefinitionLinks = (definitionID: string) => (source: Observable>): Observable => source.pipe( diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts index 196134224c..062b41a440 100644 --- a/src/app/shared/browse-by/browse-by.component.ts +++ b/src/app/shared/browse-by/browse-by.component.ts @@ -17,6 +17,9 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode fadeInOut ] }) +/** + * Component to display a browse-by page for any ListableObject + */ export class BrowseByComponent { @Input() title: string; @Input() objects$: Observable>>; 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 deleted file mode 100644 index c05c4c10c9..0000000000 --- a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - {{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 deleted file mode 100644 index 45a533cd01..0000000000 --- a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.scss +++ /dev/null @@ -1 +0,0 @@ -@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 deleted file mode 100644 index 8098a6a8c9..0000000000 --- a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 12e140c7bf..0000000000 --- a/src/app/shared/object-list/metadata-list-element/metadata-list-element.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 223eab4c69..7ae6163bf1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -72,7 +72,6 @@ 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'; @@ -149,7 +148,6 @@ const COMPONENTS = [ const ENTRY_COMPONENTS = [ // put shared entry components (components that are created dynamically) here ItemListElementComponent, - MetadataListElementComponent, CollectionListElementComponent, CommunityListElementComponent, SearchResultListElementComponent, From 375742a000568ee4d6a928ecb8abf512801bcaf7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 10 Aug 2018 13:34:44 +0200 Subject: [PATCH 03/22] count badges next to browse-by-metadata values --- .../browse-entry-list-element.component.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 9eedb70eab..198e79b453 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,3 +1,7 @@ - - {{object.value}} - +
+ + {{object.value}} + +   + {{object.count}} +
From 8548aef2b035a04a3f25175d8b2f0cca6302b5cb Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 16 Aug 2018 15:39:31 +0200 Subject: [PATCH 04/22] test fixes for browse pages --- src/app/core/data/data.service.spec.ts | 30 +++++++++----------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 8377afe92e..7af06ff62a 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs/Observable'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; -const LINK_NAME = 'test' +const endpoint = 'https://rest.api/core'; // tslint:disable:max-classes-per-file class NormalizedTestObject extends NormalizedObject { @@ -28,10 +28,9 @@ class TestService extends DataService { super(); } - public getScopedEndpoint(scope: string): Observable { - throw new Error('getScopedEndpoint is abstract in DataService'); + public getBrowseEndpoint(options: FindAllOptions): Observable { + return Observable.of(endpoint); } - } describe('DataService', () => { @@ -42,7 +41,6 @@ describe('DataService', () => { const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; - const endpoint = 'https://rest.api/core'; function initTestService(): TestService { return new TestService( @@ -50,7 +48,7 @@ describe('DataService', () => { requestService, rdbService, store, - LINK_NAME, + endpoint, halService ); } @@ -62,25 +60,17 @@ describe('DataService', () => { it('should return an observable with the endpoint', () => { options = {}; - (service as any).getFindAllHref(endpoint).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(endpoint); } ); }); - // getScopedEndpoint is not implemented in abstract DataService - it('should throw error if scopeID provided in options', () => { - options = { scopeID: 'somevalue' }; - - expect(() => { (service as any).getFindAllHref(endpoint, options) }) - .toThrowError('getScopedEndpoint is abstract in DataService'); - }); - it('should include page in href if currentPage provided in options', () => { options = { currentPage: 2 }; const expected = `${endpoint}?page=${options.currentPage - 1}`; - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -89,7 +79,7 @@ describe('DataService', () => { options = { elementsPerPage: 5 }; const expected = `${endpoint}?size=${options.elementsPerPage}`; - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -99,7 +89,7 @@ describe('DataService', () => { options = { sort: sortOptions}; const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -108,7 +98,7 @@ describe('DataService', () => { options = { startsWith: 'ab' }; const expected = `${endpoint}?startsWith=${options.startsWith}`; - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -124,7 +114,7 @@ describe('DataService', () => { const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + (service as any).getFindAllHref(options).subscribe((value) => { expect(value).toBe(expected); }); }) From 535034ec8e87a72200810f3255df2d73765d7711 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 29 Aug 2018 12:46:20 +0200 Subject: [PATCH 05/22] Added type docs --- src/app/core/data/browse-items-response-parsing-service.ts | 6 ++++++ src/app/core/data/item-data.service.ts | 6 ++++++ .../browse-entry-list-element.component.ts | 5 +++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index 840d59b863..6e3c7b4723 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -30,6 +30,12 @@ export class BrowseItemsResponseParsingService extends BaseResponseParsingServic ) { super(); } + /** + * Parses data from the browse endpoint to a list of DSpaceObjects + * @param {RestRequest} request + * @param {DSpaceRESTV2Response} data + * @returns {RestResponse} + */ 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]])) { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index e86003f308..f984dceb12 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -31,6 +31,12 @@ export class ItemDataService extends DataService { super(); } + /** + * Get the endpoint for browsing items + * (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued') + * @param {FindAllOptions} options + * @returns {Observable} + */ public getBrowseEndpoint(options: FindAllOptions = {}): Observable { let field = 'dc.date.issued'; if (options.sort && options.sort.field) { 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 index a5c92e87ae..6581d4c4ae 100644 --- 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 @@ -4,8 +4,6 @@ import { AbstractListableElementComponent } from '../../object-collection/shared 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', @@ -13,5 +11,8 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m templateUrl: './browse-entry-list-element.component.html' }) +/** + * This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent + */ @renderElementsFor(BrowseEntry, ViewMode.List) export class BrowseEntryListElementComponent extends AbstractListableElementComponent {} From 28d7a169fc3b88200daf4c0f3097cb70f09aecd3 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 30 Aug 2018 10:05:09 +0200 Subject: [PATCH 06/22] removed browse-by pages empty test files --- .../browse-by-author-page.component.spec.ts | 0 .../+browse-by-title-page/browse-by-title-page.component.spec.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.spec.ts delete mode 100644 src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts 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 deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index e69de29bb2..0000000000 From e62524e7d0341983dd95dac84a757b7d86073010 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 30 Aug 2018 12:53:57 +0200 Subject: [PATCH 07/22] fixed typos and added TypeDocs --- config/environment.default.js | 4 ++-- .../+collection-page/collection-page.component.html | 2 +- src/app/+collection-page/collection-page.resolver.ts | 10 +++++++++- src/app/+community-page/community-page.resolver.ts | 10 +++++++++- src/app/+item-page/item-page.resolver.ts | 9 +++++++++ 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 22a70f3513..a6ef738f41 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -8,13 +8,13 @@ module.exports = { nameSpace: '/' }, // The REST API server settings. - rest: { + rest: { ssl: true, host: 'dspace7.4science.it', port: 443, // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/dspace-spring-rest/api' - }, + }, // Caching settings cache: { // NOTE: how long should objects be cached for by default diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 7b56d2307c..a233163070 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -35,7 +35,7 @@
- +
diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index 3204b8d5c3..c049901bf2 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -6,13 +6,21 @@ import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; import { getSucceededRemoteData } from '../core/shared/operators'; +/** + * This class represents a resolver that requests a specific collection before the route is activated + */ @Injectable() export class CollectionPageResolver implements Resolve> { constructor(private collectionService: CollectionDataService) { } + /** + * Method for resolving a collection based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found collection based on the parameters in the current route + */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.collectionService.findById(route.params.id).pipe( getSucceededRemoteData() ); diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index 1b92b19765..917f37a821 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -6,13 +6,21 @@ import { getSucceededRemoteData } from '../core/shared/operators'; import { Community } from '../core/shared/community.model'; import { CommunityDataService } from '../core/data/community-data.service'; +/** + * This class represents a resolver that requests a specific community before the route is activated + */ @Injectable() export class CommunityPageResolver implements Resolve> { constructor(private communityService: CommunityDataService) { } + /** + * Method for resolving a community based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found community based on the parameters in the current route + */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.communityService.findById(route.params.id).pipe( getSucceededRemoteData() ); diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 2908a0ce1e..c0f4147f47 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -6,11 +6,20 @@ import { getSucceededRemoteData } from '../core/shared/operators'; import { ItemDataService } from '../core/data/item-data.service'; import { Item } from '../core/shared/item.model'; +/** + * This class represents a resolver that requests a specific item before the route is activated + */ @Injectable() export class ItemPageResolver implements Resolve> { constructor(private itemService: ItemDataService) { } + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found item based on the parameters in the current route + */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.itemService.findById(route.params.id).pipe( getSucceededRemoteData() From d7582edfa19a762eb2b3ff0ebf3dbfe957055c70 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Sep 2018 11:24:37 +0200 Subject: [PATCH 08/22] BrowseItemsResponseParsingService TypeDocs --- src/app/core/data/browse-items-response-parsing-service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index 6e3c7b4723..e513ad0898 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -16,6 +16,9 @@ import { RestRequest } from './request.models'; import { Item } from '../shared/item.model'; import { DSpaceObject } from '../shared/dspace-object.model'; +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[]) + */ @Injectable() export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { From cb20a31379be97d7533bf334c8e5f91b9b86f3fc Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Sep 2018 12:05:22 +0200 Subject: [PATCH 09/22] Fixed Browse-By-Title sort direction --- .../+browse-by-title-page/browse-by-title-page.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 3205091952..1759264e2a 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 @@ -54,14 +54,15 @@ export class BrowseByTitlePageComponent implements OnInit { .subscribe((params) => { const page = +params.page || this.paginationConfig.currentPage; const pageSize = +params.pageSize || this.paginationConfig.pageSize; - const sortDirection = +params.page || this.sortConfig.direction; + 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: params.sortField } + { direction: sortDirection, field: sortField } ); this.updatePage({ pagination: pagination, From caf9194f3627b69a391b235b71b14d5e387cb511 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 12 Sep 2018 11:38:08 +0200 Subject: [PATCH 10/22] Fixes for authentication (awaiting fixes in EPerson REST endpoint) --- src/app/core/auth/auth-object-factory.ts | 8 +++--- .../auth-response-parsing.service.spec.ts | 19 +++++++++---- .../auth/auth-response-parsing.service.ts | 9 +++--- src/app/core/auth/auth.effects.spec.ts | 1 - src/app/core/auth/auth.service.spec.ts | 10 +++++-- src/app/core/auth/auth.service.ts | 28 ++++++++++++++----- src/app/core/auth/models/auth-status.model.ts | 5 ++-- .../models/normalized-auth-status.model.ts | 22 +++++++++------ src/app/core/auth/server-auth.service.ts | 11 ++++---- .../cache/models/normalized-object-factory.ts | 8 ++++++ src/app/core/data/data.service.ts | 7 +++-- .../eperson/models/NormalizedEperson.model.ts | 2 +- .../eperson/models/NormalizedGroup.model.ts | 3 +- .../auth-nav-menu/auth-nav-menu.component.ts | 1 + .../mocks/mock-remote-data-build.service.ts | 4 ++- .../testing/auth-request-service-stub.ts | 5 ++-- src/app/shared/testing/auth-service-stub.ts | 3 +- 17 files changed, 96 insertions(+), 50 deletions(-) diff --git a/src/app/core/auth/auth-object-factory.ts b/src/app/core/auth/auth-object-factory.ts index c3e70eaaac..02330ca5b3 100644 --- a/src/app/core/auth/auth-object-factory.ts +++ b/src/app/core/auth/auth-object-factory.ts @@ -1,14 +1,14 @@ import { AuthType } from './auth-type'; import { GenericConstructor } from '../shared/generic-constructor'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; -import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; -import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model'; +import { NormalizedEperson } from '../eperson/models/NormalizedEperson.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; export class AuthObjectFactory { - public static getConstructor(type): GenericConstructor { + public static getConstructor(type): GenericConstructor { switch (type) { case AuthType.Eperson: { - return NormalizedEpersonModel + return NormalizedEperson } case AuthType.Status: { diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index f7d899a9bc..f6dd87e99a 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -8,12 +8,13 @@ import { CoreState } from '../core.reducers'; import { AuthStatus } from './models/auth-status.model'; import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; +import { getMockStore } from '../../shared/mocks/mock-store'; -describe('ConfigResponseParsingService', () => { +describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; - const EnvConfig = {} as GlobalConfig; - const store = {} as Store; + const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig; + const store = getMockStore() as Store; const objectCacheService = new ObjectCacheService(store); beforeEach(() => { @@ -86,13 +87,19 @@ describe('ConfigResponseParsingService', () => { type: 'eperson', uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', _links: { - self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' + self: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' + } } } }, _links: { - eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b', - self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' + eperson: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' + }, + self: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' + } } }, statusCode: '200' diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 80c1b2eeca..8efa36f9e2 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -12,22 +12,23 @@ import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/request.models'; import { AuthType } from './auth-type'; import { AuthStatus } from './models/auth-status.model'; +import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; @Injectable() export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected objectFactory = AuthObjectFactory; - protected toCache = false; + protected toCache = true; constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService,) { + protected objectCache: ObjectCacheService) { super(); } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) { - const response = this.process(data.payload, request.href); - return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode); + const response = this.process(data.payload, request.href); + return new AuthStatusResponse(response, data.statusCode); } else { return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode); } diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 3b569e523f..b862ae77fe 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -30,7 +30,6 @@ import { EpersonMock } from '../../shared/testing/eperson-mock'; describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; - const authServiceStub = new AuthServiceStub(); const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index b54f65078e..b238bef033 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -22,6 +22,8 @@ import { Eperson } from '../eperson/models/eperson.model'; import { EpersonMock } from '../../shared/testing/eperson-mock'; import { AppState } from '../../app.reducer'; import { ClientCookieService } from '../../shared/services/client-cookie.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; describe('AuthService test', () => { @@ -44,7 +46,7 @@ describe('AuthService test', () => { authToken: token, user: EpersonMock }; - + const rdbService = getMockRemoteDataBuildService(); describe('', () => { beforeEach(() => { @@ -61,6 +63,7 @@ describe('AuthService test', () => { {provide: Router, useValue: routerStub}, {provide: ActivatedRoute, useValue: routeStub}, {provide: Store, useValue: mockStore}, + {provide: RemoteDataBuildService, useValue: rdbService}, CookieService, AuthService ], @@ -121,6 +124,7 @@ describe('AuthService test', () => { {provide: AuthRequestService, useValue: authRequest}, {provide: REQUEST, useValue: {}}, {provide: Router, useValue: routerStub}, + {provide: RemoteDataBuildService, useValue: rdbService}, CookieService ] }).compileComponents(); @@ -132,7 +136,7 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, authReqService, router, cookieService, store); + authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService); })); it('should return true when user is logged in', () => { @@ -191,7 +195,7 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, authReqService, router, cookieService, store); + authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService); storage = (authService as any).storage; spyOn(storage, 'get'); spyOn(storage, 'remove'); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 2848b54b50..f19651e0dd 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -7,7 +7,7 @@ import { RouterReducerState } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; import { Observable } from 'rxjs/Observable'; -import { map, withLatestFrom } from 'rxjs/operators'; +import { map, switchMap, withLatestFrom } from 'rxjs/operators'; import { Eperson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; @@ -17,11 +17,18 @@ import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import { CookieService } from '../../shared/services/cookie.service'; -import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; +import { + getAuthenticationToken, + getRedirectUrl, + isAuthenticated, + isTokenRefreshing +} from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedEperson } from '../eperson/models/NormalizedEperson.model'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -45,7 +52,9 @@ export class AuthService { protected authRequestService: AuthRequestService, protected router: Router, protected storage: CookieService, - protected store: Store) { + protected store: Store, + protected rdbService: RemoteDataBuildService + ) { this.store.select(isAuthenticated) .startWith(false) .subscribe((authenticated: boolean) => this._authenticated = authenticated); @@ -123,14 +132,19 @@ export class AuthService { headers = headers.append('Accept', 'application/json'); headers = headers.append('Authorization', `Bearer ${token.accessToken}`); options.headers = headers; - return this.authRequestService.getRequest('status', options) - .map((status: AuthStatus) => { + return this.authRequestService.getRequest('status', options).pipe( + switchMap((status: AuthStatus) => { + if (status.authenticated) { - return status.eperson[0]; + + // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... + const person$ = this.rdbService.buildSingle(status.eperson.toString()); + // person$.subscribe(() => console.log('test')); + return person$.pipe(map((eperson) => eperson.payload)); } else { throw(new Error('Not authenticated')); } - }); + })) } /** diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 22c9d14718..bf90b82bf6 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,7 +1,8 @@ import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; -import { DSpaceObject } from '../../shared/dspace-object.model'; import { Eperson } from '../../eperson/models/eperson.model'; +import { RemoteData } from '../../data/remote-data'; +import { Observable } from 'rxjs/Observable'; export class AuthStatus { @@ -13,7 +14,7 @@ export class AuthStatus { error?: AuthError; - eperson: Eperson[]; + eperson: Observable>; token?: AuthTokenInfo; diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index 19952f7c70..f7f6ab5a9e 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -1,12 +1,18 @@ import { AuthStatus } from './auth-status.model'; import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { Eperson } from '../../eperson/models/eperson.model'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { NormalizedObject } from '../../cache/models/normalized-object.model'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; @mapsTo(AuthStatus) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedAuthStatus extends NormalizedDSpaceObject { +@inheritSerialization(NormalizedObject) +export class NormalizedAuthStatus extends NormalizedObject { + @autoserialize + id: string; + + @autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id') + uuid: string; /** * True if REST API is up and running, should never return false @@ -20,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject { @autoserialize authenticated: boolean; - @autoserializeAs(Eperson) - eperson: Eperson[]; - + @relationship(ResourceType.Eperson, false) + @autoserialize + eperson: string; } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 96ee2e355a..833df4b9d2 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,3 +1,4 @@ +import {first, map} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -32,14 +33,14 @@ export class ServerAuthService extends AuthService { headers = headers.append('X-Forwarded-For', clientIp); options.headers = headers; - return this.authRequestService.getRequest('status', options) - .map((status: AuthStatus) => { + return this.authRequestService.getRequest('status', options).pipe( + map((status: AuthStatus) => { if (status.authenticated) { return status.eperson[0]; } else { throw(new Error('Not authenticated')); } - }); + })); } /** @@ -53,8 +54,8 @@ export class ServerAuthService extends AuthService { * Redirect to the route navigated before the login */ public redirectToPreviousUrl() { - this.getRedirectUrl() - .first() + this.getRedirectUrl().pipe( + first()) .subscribe((redirectUrl) => { if (isNotEmpty(redirectUrl)) { // override the route reuse strategy diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index df67a1f2ce..fc35dffca8 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -8,6 +8,8 @@ import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from './normalized-object.model'; import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; +import { NormalizedEperson } from '../../eperson/models/NormalizedEperson.model'; +import { NormalizedGroup } from '../../eperson/models/NormalizedGroup.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor { @@ -33,6 +35,12 @@ export class NormalizedObjectFactory { case ResourceType.ResourcePolicy: { return NormalizedResourcePolicy } + case ResourceType.Eperson: { + return NormalizedEperson + } + case ResourceType.Group: { + return NormalizedGroup + } default: { return undefined; } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index c7588a5231..c6b2c51134 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,3 +1,4 @@ +import { filter, take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -60,9 +61,9 @@ export abstract class DataService const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href)) .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); - hrefObs - .filter((href: string) => hasValue(href)) - .take(1) + hrefObs.pipe( + filter((href: string) => hasValue(href)), + take(1)) .subscribe((href: string) => { const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); this.requestService.configure(request); diff --git a/src/app/core/eperson/models/NormalizedEperson.model.ts b/src/app/core/eperson/models/NormalizedEperson.model.ts index 0c0b2490d6..bdcd069eb8 100644 --- a/src/app/core/eperson/models/NormalizedEperson.model.ts +++ b/src/app/core/eperson/models/NormalizedEperson.model.ts @@ -8,7 +8,7 @@ import { ResourceType } from '../../shared/resource-type'; @mapsTo(Eperson) @inheritSerialization(NormalizedDSpaceObject) -export class NormalizedEpersonModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject { +export class NormalizedEperson extends NormalizedDSpaceObject implements CacheableObject, ListableObject { @autoserialize public handle: string; diff --git a/src/app/core/eperson/models/NormalizedGroup.model.ts b/src/app/core/eperson/models/NormalizedGroup.model.ts index 24f7da8eab..be5995d9c5 100644 --- a/src/app/core/eperson/models/NormalizedGroup.model.ts +++ b/src/app/core/eperson/models/NormalizedGroup.model.ts @@ -2,13 +2,12 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { Eperson } from './eperson.model'; import { mapsTo } from '../../cache/builders/build-decorators'; import { Group } from './group.model'; @mapsTo(Group) @inheritSerialization(NormalizedDSpaceObject) -export class NormalizedGroupModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject { +export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject, ListableObject { @autoserialize public handle: string; diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index 1c376258fb..05dfd2d872 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -10,6 +10,7 @@ import { isNotUndefined } from '../empty.util'; import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; import { Eperson } from '../../core/eperson/models/eperson.model'; import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; +import { RemoteData } from '../../core/data/remote-data'; @Component({ selector: 'ds-auth-nav-menu', diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts index c10032eb94..8f93fe2d96 100644 --- a/src/app/shared/mocks/mock-remote-data-build.service.ts +++ b/src/app/shared/mocks/mock-remote-data-build.service.ts @@ -5,6 +5,7 @@ import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { RemoteData } from '../../core/data/remote-data'; import { RequestEntry } from '../../core/data/request.reducer'; import { hasValue } from '../empty.util'; +import { NormalizedObject } from '../../core/cache/models/normalized-object.model'; export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable>): RemoteDataBuildService { return { @@ -17,7 +18,8 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab payload } as RemoteData))) } - } + }, + buildSingle: (href$: string | Observable) => Observable.of(new RemoteData(false, false, true, undefined, {})) } as RemoteDataBuildService; } diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts index 2c47068af4..4f525463c5 100644 --- a/src/app/shared/testing/auth-request-service-stub.ts +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -5,6 +5,7 @@ import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { Eperson } from '../../core/eperson/models/eperson.model'; import { isNotEmpty } from '../empty.util'; import { EpersonMock } from './eperson-mock'; +import { RemoteData } from '../../core/data/remote-data'; export class AuthRequestServiceStub { protected mockUser: Eperson = EpersonMock; @@ -26,7 +27,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = [this.mockUser]; + authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } @@ -45,7 +46,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = [this.mockUser]; + authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index c7d5556910..07157c8623 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -3,6 +3,7 @@ import { Observable } from 'rxjs/Observable'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { EpersonMock } from './eperson-mock'; import { Eperson } from '../../core/eperson/models/eperson.model'; +import { RemoteData } from '../../core/data/remote-data'; export class AuthServiceStub { @@ -19,7 +20,7 @@ export class AuthServiceStub { authStatus.okay = true; authStatus.authenticated = true; authStatus.token = this.token; - authStatus.eperson = [EpersonMock]; + authStatus.eperson = Observable.of(new RemoteData(false, false, true, undefined, EpersonMock)); return Observable.of(authStatus); } else { console.log('error'); From 1f336f29fdf6941d8f35ddb94a0a7715008cfafc Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 12 Sep 2018 16:11:04 +0200 Subject: [PATCH 11/22] small fixes for authentication --- src/app/core/auth/auth-object-factory.ts | 7 ++--- src/app/core/auth/auth-type.ts | 2 +- src/app/core/auth/auth.actions.ts | 14 +++++----- src/app/core/auth/auth.effects.spec.ts | 4 +-- src/app/core/auth/auth.effects.ts | 6 ++--- src/app/core/auth/auth.reducer.spec.ts | 26 +++++++++---------- src/app/core/auth/auth.reducer.ts | 4 +-- src/app/core/auth/auth.service.spec.ts | 10 +++---- src/app/core/auth/auth.service.ts | 14 +++++----- src/app/core/auth/models/auth-status.model.ts | 4 +-- .../models/normalized-auth-status.model.ts | 2 +- src/app/core/auth/server-auth.service.ts | 20 +++++++++----- .../cache/models/normalized-object-factory.ts | 8 +++--- .../data/base-response-parsing.service.ts | 22 ++++++++++++++++ src/app/core/eperson/models/eperson.model.ts | 2 +- ...n.model.ts => normalized-eperson.model.ts} | 6 ++--- ...oup.model.ts => normalized-group.model.ts} | 0 src/app/core/shared/resource-type.ts | 2 +- .../auth-nav-menu.component.spec.ts | 5 ++-- .../auth-nav-menu/auth-nav-menu.component.ts | 4 +-- .../shared/log-in/log-in.component.spec.ts | 8 +++--- .../shared/log-out/log-out.component.spec.ts | 8 +++--- .../testing/auth-request-service-stub.ts | 10 +++---- src/app/shared/testing/auth-service-stub.ts | 10 +++---- src/app/shared/testing/eperson-mock.ts | 4 +-- 25 files changed, 115 insertions(+), 87 deletions(-) rename src/app/core/eperson/models/{NormalizedEperson.model.ts => normalized-eperson.model.ts} (88%) rename src/app/core/eperson/models/{NormalizedGroup.model.ts => normalized-group.model.ts} (100%) diff --git a/src/app/core/auth/auth-object-factory.ts b/src/app/core/auth/auth-object-factory.ts index 02330ca5b3..b6df1fac34 100644 --- a/src/app/core/auth/auth-object-factory.ts +++ b/src/app/core/auth/auth-object-factory.ts @@ -1,14 +1,15 @@ import { AuthType } from './auth-type'; import { GenericConstructor } from '../shared/generic-constructor'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; -import { NormalizedEperson } from '../eperson/models/NormalizedEperson.model'; +import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { EPerson } from '../eperson/models/eperson.model'; export class AuthObjectFactory { public static getConstructor(type): GenericConstructor { switch (type) { - case AuthType.Eperson: { - return NormalizedEperson + case AuthType.EPerson: { + return NormalizedEPerson } case AuthType.Status: { diff --git a/src/app/core/auth/auth-type.ts b/src/app/core/auth/auth-type.ts index b8879ae445..9a248da91f 100644 --- a/src/app/core/auth/auth-type.ts +++ b/src/app/core/auth/auth-type.ts @@ -1,4 +1,4 @@ export enum AuthType { - Eperson = 'eperson', + EPerson = 'eperson', Status = 'status' } diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 207e8fae70..d0969d38d4 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -5,7 +5,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; // import models -import { Eperson } from '../eperson/models/eperson.model'; +import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; export const AuthActionTypes = { @@ -76,10 +76,10 @@ export class AuthenticatedSuccessAction implements Action { payload: { authenticated: boolean; authToken: AuthTokenInfo; - user: Eperson + user: EPerson }; - constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) { + constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) { this.payload = { authenticated, authToken, user }; } } @@ -250,9 +250,9 @@ export class RefreshTokenErrorAction implements Action { */ export class RegistrationAction implements Action { public type: string = AuthActionTypes.REGISTRATION; - payload: Eperson; + payload: EPerson; - constructor(user: Eperson) { + constructor(user: EPerson) { this.payload = user; } } @@ -278,9 +278,9 @@ export class RegistrationErrorAction implements Action { */ export class RegistrationSuccessAction implements Action { public type: string = AuthActionTypes.REGISTRATION_SUCCESS; - payload: Eperson; + payload: EPerson; - constructor(user: Eperson) { + constructor(user: EPerson) { this.payload = user; } } diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index b862ae77fe..d75b407516 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -25,7 +25,7 @@ import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; import { AuthService } from './auth.service'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; -import { EpersonMock } from '../../shared/testing/eperson-mock'; +import { EPersonMock } from '../../shared/testing/eperson-mock'; describe('AuthEffects', () => { let authEffects: AuthEffects; @@ -104,7 +104,7 @@ describe('AuthEffects', () => { it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => { actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}}); - const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)}); + const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)}); expect(authEffects.authenticated$).toBeObservable(expected); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index e2d6c80b5e..82def702eb 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -28,7 +28,7 @@ import { RegistrationErrorAction, RegistrationSuccessAction } from './auth.actions'; -import { Eperson } from '../eperson/models/eperson.model'; +import { EPerson } from '../eperson/models/eperson.model'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AppState } from '../../app.reducer'; @@ -63,7 +63,7 @@ export class AuthEffects { .ofType(AuthActionTypes.AUTHENTICATED) .switchMap((action: AuthenticatedAction) => { return this.authService.authenticatedUser(action.payload) - .map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)) + .map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)) .catch((error) => Observable.of(new AuthenticatedErrorAction(error))); }); @@ -88,7 +88,7 @@ export class AuthEffects { .debounceTime(500) // to remove when functionality is implemented .switchMap((action: RegistrationAction) => { return this.authService.create(action.payload) - .map((user: Eperson) => new RegistrationSuccessAction(user)) + .map((user: EPerson) => new RegistrationSuccessAction(user)) .catch((error) => Observable.of(new RegistrationErrorAction(error))); }); diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index f148f3ac8d..ca2ba00036 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -21,7 +21,7 @@ import { SetRedirectUrlAction } from './auth.actions'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EpersonMock } from '../../shared/testing/eperson-mock'; +import { EPersonMock } from '../../shared/testing/eperson-mock'; describe('authReducer', () => { @@ -107,7 +107,7 @@ describe('authReducer', () => { loading: true, info: undefined }; - const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock); + const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock); const newState = authReducer(initialState, action); state = { authenticated: true, @@ -116,7 +116,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; expect(newState).toEqual(state); }); @@ -182,7 +182,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; const action = new LogOutAction(); @@ -199,7 +199,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; const action = new LogOutSuccessAction(); @@ -225,7 +225,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; const action = new LogOutErrorAction(mockError); @@ -237,7 +237,7 @@ describe('authReducer', () => { error: 'Test error message', loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; expect(newState).toEqual(state); }); @@ -250,7 +250,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenAction(newTokenInfo); @@ -262,7 +262,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock, + user: EPersonMock, refreshing: true }; expect(newState).toEqual(state); @@ -276,7 +276,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock, + user: EPersonMock, refreshing: true }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); @@ -289,7 +289,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock, + user: EPersonMock, refreshing: false }; expect(newState).toEqual(state); @@ -303,7 +303,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock, + user: EPersonMock, refreshing: true }; const action = new RefreshTokenErrorAction(); @@ -329,7 +329,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EpersonMock + user: EPersonMock }; state = { diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 0c5e36ce91..98827d842e 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -12,7 +12,7 @@ import { SetRedirectUrlAction } from './auth.actions'; // import models -import { Eperson } from '../eperson/models/eperson.model'; +import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; /** @@ -46,7 +46,7 @@ export interface AuthState { refreshing?: boolean; // the authenticated user - user?: Eperson; + user?: EPerson; } /** diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index b238bef033..c943a815e7 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -18,8 +18,8 @@ import { AuthRequestServiceStub } from '../../shared/testing/auth-request-servic import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { Eperson } from '../eperson/models/eperson.model'; -import { EpersonMock } from '../../shared/testing/eperson-mock'; +import { EPerson } from '../eperson/models/eperson.model'; +import { EPersonMock } from '../../shared/testing/eperson-mock'; import { AppState } from '../../app.reducer'; import { ClientCookieService } from '../../shared/services/client-cookie.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -44,7 +44,7 @@ describe('AuthService test', () => { loaded: true, loading: false, authToken: token, - user: EpersonMock + user: EPersonMock }; const rdbService = getMockRemoteDataBuildService(); describe('', () => { @@ -82,7 +82,7 @@ describe('AuthService test', () => { }); it('should return the authenticated user object when user token is valid', () => { - authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => { + authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => { expect(user).toBeDefined(); }); }); @@ -188,7 +188,7 @@ describe('AuthService test', () => { loaded: true, loading: false, authToken: expiredToken, - user: EpersonMock + user: EPersonMock }; store .subscribe((state) => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f19651e0dd..ea73ff9e1b 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -9,7 +9,7 @@ import { CookieAttributes } from 'js-cookie'; import { Observable } from 'rxjs/Observable'; import { map, switchMap, withLatestFrom } from 'rxjs/operators'; -import { Eperson } from '../eperson/models/eperson.model'; +import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -28,7 +28,7 @@ import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth. import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedEperson } from '../eperson/models/NormalizedEperson.model'; +import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -107,7 +107,7 @@ export class AuthService { if (status.authenticated) { return status; } else { - throw(new Error('Invalid email or password')); + Observable.throw(new Error('Invalid email or password')); } }) @@ -125,7 +125,7 @@ export class AuthService { * Returns the authenticated user * @returns {User} */ - public authenticatedUser(token: AuthTokenInfo): Observable { + public authenticatedUser(token: AuthTokenInfo): Observable { // Determine if the user has an existing auth session on the server const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -136,10 +136,8 @@ export class AuthService { switchMap((status: AuthStatus) => { if (status.authenticated) { - // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... - const person$ = this.rdbService.buildSingle(status.eperson.toString()); - // person$.subscribe(() => console.log('test')); + const person$ = this.rdbService.buildSingle(status.eperson.toString()); return person$.pipe(map((eperson) => eperson.payload)); } else { throw(new Error('Not authenticated')); @@ -202,7 +200,7 @@ export class AuthService { * Create a new user * @returns {User} */ - public create(user: Eperson): Observable { + public create(user: EPerson): Observable { // Normally you would do an HTTP request to POST the user // details and then return the new user object // but, let's just return the new user for this example. diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index bf90b82bf6..b8ccf9ed6d 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,6 +1,6 @@ import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; -import { Eperson } from '../../eperson/models/eperson.model'; +import { EPerson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data'; import { Observable } from 'rxjs/Observable'; @@ -14,7 +14,7 @@ export class AuthStatus { error?: AuthError; - eperson: Observable>; + eperson: Observable>; token?: AuthTokenInfo; diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index f7f6ab5a9e..b8dd2aa23e 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -26,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedObject { @autoserialize authenticated: boolean; - @relationship(ResourceType.Eperson, false) + @relationship(ResourceType.EPerson, false) @autoserialize eperson: string; } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 833df4b9d2..8d1bf1673c 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,4 +1,4 @@ -import {first, map} from 'rxjs/operators'; +import { first, map, switchMap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @@ -9,7 +9,8 @@ import { isNotEmpty } from '../../shared/empty.util'; import { AuthService } from './auth.service'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { CheckAuthenticationTokenAction } from './auth.actions'; -import { Eperson } from '../eperson/models/eperson.model'; +import { EPerson } from '../eperson/models/eperson.model'; +import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model'; /** * The auth service. @@ -21,7 +22,7 @@ export class ServerAuthService extends AuthService { * Returns the authenticated user * @returns {User} */ - public authenticatedUser(token: AuthTokenInfo): Observable { + public authenticatedUser(token: AuthTokenInfo): Observable { // Determine if the user has an existing auth session on the server const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -34,13 +35,18 @@ export class ServerAuthService extends AuthService { options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status: AuthStatus) => { + switchMap((status: AuthStatus) => { + if (status.authenticated) { - return status.eperson[0]; + + // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... + const person$ = this.rdbService.buildSingle(status.eperson.toString()); + // person$.subscribe(() => console.log('test')); + return person$.pipe(map((eperson) => eperson.payload)); } else { - throw(new Error('Not authenticated')); + Observable.throw(new Error('Not authenticated')); } - })); + })) } /** diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index fc35dffca8..5c5ebf50aa 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -8,8 +8,8 @@ import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from './normalized-object.model'; import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; -import { NormalizedEperson } from '../../eperson/models/NormalizedEperson.model'; -import { NormalizedGroup } from '../../eperson/models/NormalizedGroup.model'; +import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model'; +import { NormalizedGroup } from '../../eperson/models/normalized-group.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor { @@ -35,8 +35,8 @@ export class NormalizedObjectFactory { case ResourceType.ResourcePolicy: { return NormalizedResourcePolicy } - case ResourceType.Eperson: { - return NormalizedEperson + case ResourceType.EPerson: { + return NormalizedEPerson } case ResourceType.Group: { return NormalizedGroup diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 050b3c2da5..bdd18f8a61 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -7,6 +7,8 @@ import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { ResourceType } from '../shared/resource-type'; +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; function isObjectLevel(halObj: any) { return isNotEmpty(halObj._links) && hasValue(halObj._links.self); @@ -34,6 +36,7 @@ export abstract class BaseResponseParsingService { } else if (Array.isArray(data)) { return this.processArray(data, requestHref); } else if (isObjectLevel(data)) { + data = this.fixBadEPersonRestResponse(data); const object = this.deserialize(data); if (isNotEmpty(data._embedded)) { Object @@ -53,6 +56,7 @@ export abstract class BaseResponseParsingService { } }); } + this.cache(object, requestHref); return object; } @@ -145,4 +149,22 @@ export abstract class BaseResponseParsingService { } return obj[keys[0]]; } + + /* TODO remove when REST response for epersons is fixed */ + private fixBadEPersonRestResponse(obj: any): any { + if (obj.type === ResourceType.EPerson) { + const groups = obj.groups; + const normGroups = []; + if (isNotEmpty(groups)) { + groups.forEach((group) => { + const parts = ['eperson', 'groups', group.uuid]; + const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString(); + normGroups.push(href); + } + ) + } + return Object.assign({}, obj, { groups: normGroups }); + } + return obj; + } } diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index 373fb42792..45d26761b0 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -1,7 +1,7 @@ import { DSpaceObject } from '../../shared/dspace-object.model'; import { Group } from './group.model'; -export class Eperson extends DSpaceObject { +export class EPerson extends DSpaceObject { public handle: string; diff --git a/src/app/core/eperson/models/NormalizedEperson.model.ts b/src/app/core/eperson/models/normalized-eperson.model.ts similarity index 88% rename from src/app/core/eperson/models/NormalizedEperson.model.ts rename to src/app/core/eperson/models/normalized-eperson.model.ts index bdcd069eb8..9d0fa428e9 100644 --- a/src/app/core/eperson/models/NormalizedEperson.model.ts +++ b/src/app/core/eperson/models/normalized-eperson.model.ts @@ -2,13 +2,13 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { Eperson } from './eperson.model'; +import { EPerson } from './eperson.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; -@mapsTo(Eperson) +@mapsTo(EPerson) @inheritSerialization(NormalizedDSpaceObject) -export class NormalizedEperson extends NormalizedDSpaceObject implements CacheableObject, ListableObject { +export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject { @autoserialize public handle: string; diff --git a/src/app/core/eperson/models/NormalizedGroup.model.ts b/src/app/core/eperson/models/normalized-group.model.ts similarity index 100% rename from src/app/core/eperson/models/NormalizedGroup.model.ts rename to src/app/core/eperson/models/normalized-group.model.ts diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index 71053f628b..e67f3339de 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -6,7 +6,7 @@ export enum ResourceType { Item = 'item', Collection = 'collection', Community = 'community', - Eperson = 'eperson', + EPerson = 'eperson', Group = 'group', ResourcePolicy = 'resourcePolicy' } diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 8b9f7c8775..1c59c02346 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -5,7 +5,7 @@ import { By } from '@angular/platform-browser'; import { Store, StoreModule } from '@ngrx/store'; import { authReducer, AuthState } from '../../core/auth/auth.reducer'; -import { EpersonMock } from '../testing/eperson-mock'; +import { EPersonMock } from '../testing/eperson-mock'; import { TranslateModule } from '@ngx-translate/core'; import { AppState } from '../../app.reducer'; import { AuthNavMenuComponent } from './auth-nav-menu.component'; @@ -13,6 +13,7 @@ import { HostWindowServiceStub } from '../testing/host-window-service-stub'; import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; describe('AuthNavMenuComponent', () => { @@ -31,7 +32,7 @@ describe('AuthNavMenuComponent', () => { loaded: true, loading: false, authToken: new AuthTokenInfo('test_token'), - user: EpersonMock + user: EPersonMock }; let routerState = { url: '/home' diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index 05dfd2d872..887729a7af 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -8,7 +8,7 @@ import { HostWindowService } from '../host-window.service'; import { AppState, routerStateSelector } from '../../app.reducer'; import { isNotUndefined } from '../empty.util'; import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -35,7 +35,7 @@ export class AuthNavMenuComponent implements OnInit { public showAuth = Observable.of(false); - public user: Observable; + public user: Observable; constructor(private store: Store, private windowService: HostWindowService) { diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts index dc4a0be1c6..dd2aea35d5 100644 --- a/src/app/shared/log-in/log-in.component.spec.ts +++ b/src/app/shared/log-in/log-in.component.spec.ts @@ -7,8 +7,8 @@ import { Store, StoreModule } from '@ngrx/store'; import { LogInComponent } from './log-in.component'; import { authReducer } from '../../core/auth/auth.reducer'; -import { EpersonMock } from '../testing/eperson-mock'; -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPersonMock } from '../testing/eperson-mock'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { TranslateModule } from '@ngx-translate/core'; import { AuthService } from '../../core/auth/auth.service'; import { AuthServiceStub } from '../testing/auth-service-stub'; @@ -19,7 +19,7 @@ describe('LogInComponent', () => { let component: LogInComponent; let fixture: ComponentFixture; let page: Page; - let user: Eperson; + let user: EPerson; const authState = { authenticated: false, @@ -28,7 +28,7 @@ describe('LogInComponent', () => { }; beforeEach(() => { - user = EpersonMock; + user = EPersonMock; }); beforeEach(async(() => { diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts index ad609f0aea..94780ead5a 100644 --- a/src/app/shared/log-out/log-out.component.spec.ts +++ b/src/app/shared/log-out/log-out.component.spec.ts @@ -5,8 +5,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Store, StoreModule } from '@ngrx/store'; import { authReducer } from '../../core/auth/auth.reducer'; -import { EpersonMock } from '../testing/eperson-mock'; -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPersonMock } from '../testing/eperson-mock'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { TranslateModule } from '@ngx-translate/core'; import { Router } from '@angular/router'; import { AppState } from '../../app.reducer'; @@ -18,7 +18,7 @@ describe('LogOutComponent', () => { let component: LogOutComponent; let fixture: ComponentFixture; let page: Page; - let user: Eperson; + let user: EPerson; const authState = { authenticated: false, @@ -28,7 +28,7 @@ describe('LogOutComponent', () => { const routerStub = new RouterStub(); beforeEach(() => { - user = EpersonMock; + user = EPersonMock; }); beforeEach(async(() => { diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts index 4f525463c5..576b8c2ddf 100644 --- a/src/app/shared/testing/auth-request-service-stub.ts +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -2,13 +2,13 @@ import { Observable } from 'rxjs/Observable'; import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { isNotEmpty } from '../empty.util'; -import { EpersonMock } from './eperson-mock'; +import { EPersonMock } from './eperson-mock'; import { RemoteData } from '../../core/data/remote-data'; export class AuthRequestServiceStub { - protected mockUser: Eperson = EpersonMock; + protected mockUser: EPerson = EPersonMock; protected mockTokenInfo = new AuthTokenInfo('test_token'); public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable { @@ -27,7 +27,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); + authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } @@ -46,7 +46,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); + authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index 07157c8623..d6c20c9520 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -1,8 +1,8 @@ import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { Observable } from 'rxjs/Observable'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; -import { EpersonMock } from './eperson-mock'; -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPersonMock } from './eperson-mock'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { RemoteData } from '../../core/data/remote-data'; export class AuthServiceStub { @@ -20,7 +20,7 @@ export class AuthServiceStub { authStatus.okay = true; authStatus.authenticated = true; authStatus.token = this.token; - authStatus.eperson = Observable.of(new RemoteData(false, false, true, undefined, EpersonMock)); + authStatus.eperson = Observable.of(new RemoteData(false, false, true, undefined, EPersonMock)); return Observable.of(authStatus); } else { console.log('error'); @@ -28,9 +28,9 @@ export class AuthServiceStub { } } - public authenticatedUser(token: AuthTokenInfo): Observable { + public authenticatedUser(token: AuthTokenInfo): Observable { if (token.accessToken === 'token_test') { - return Observable.of(EpersonMock); + return Observable.of(EPersonMock); } else { throw(new Error('Message Error test')); } diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index 9cf938fcf2..f163a490b9 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -1,6 +1,6 @@ -import { Eperson } from '../../core/eperson/models/eperson.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; -export const EpersonMock: Eperson = Object.assign(new Eperson(),{ +export const EPersonMock: EPerson = Object.assign(new EPerson(),{ handle: null, groups: [], netid: 'test@test.com', From 2b1d4cd12a6c16e10ddd06d6e269bfd31a21b45c Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 14 Sep 2018 11:42:55 +0200 Subject: [PATCH 12/22] Fixed logout issue --- src/app/core/auth/auth.interceptor.ts | 2 +- src/app/core/auth/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 651e2fd096..f38abb90bc 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -35,7 +35,7 @@ export class AuthInterceptor implements HttpInterceptor { } private isSuccess(response: HttpResponseBase): boolean { - return response.status === 200; + return (response.status === 200 || response.status === 204); } private isAuthRequest(http: HttpRequest | HttpResponseBase): boolean { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index ea73ff9e1b..f8fc863ee9 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -140,7 +140,7 @@ export class AuthService { const person$ = this.rdbService.buildSingle(status.eperson.toString()); return person$.pipe(map((eperson) => eperson.payload)); } else { - throw(new Error('Not authenticated')); + Observable.throw(new Error('Not authenticated')); } })) } From 97669d0c346b11055ae322069960d65364e989b2 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 17 Sep 2018 10:20:12 +0200 Subject: [PATCH 13/22] upgrade ngrx to fix store devtools bug --- package.json | 6 +++--- yarn.lock | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 0936b27ea4..e1ff94b428 100644 --- a/package.json +++ b/package.json @@ -90,8 +90,8 @@ "@ngx-translate/http-loader": "2.0.1", "@nicky-lenaers/ngx-scroll-to": "^0.6.0", "angular-idle-preload": "2.0.4", - "angular2-moment": "^1.9.0", "angular-sortablejs": "^2.5.0", + "angular2-moment": "^1.9.0", "angular2-text-mask": "8.0.4", "angulartics2": "^5.2.0", "body-parser": "1.18.2", @@ -99,7 +99,7 @@ "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", - "core-js": "2.5.3", + "core-js": "^2.5.7", "express": "4.16.2", "express-session": "1.15.6", "font-awesome": "4.7.0", @@ -112,8 +112,8 @@ "methods": "1.1.2", "moment": "^2.22.1", "morgan": "1.9.0", - "ng2-nouislider": "^1.7.11", "ng2-file-upload": "1.2.1", + "ng2-nouislider": "^1.7.11", "ngx-infinite-scroll": "0.8.2", "ngx-pagination": "3.0.3", "nouislider": "^11.0.0", diff --git a/yarn.lock b/yarn.lock index 344093d446..5caaac6090 100644 --- a/yarn.lock +++ b/yarn.lock @@ -90,20 +90,20 @@ resolved "https://registry.yarnpkg.com/@ng-dynamic-forms/ui-ng-bootstrap/-/ui-ng-bootstrap-5.4.7.tgz#66d037a226da96fe84c4dbac98e4dba859c551f8" "@ngrx/effects@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-5.1.0.tgz#cef84576b2d0333f19188aedfe156fd301bff70a" + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-5.2.0.tgz#aa762b69cb6fd4644d724a1cecd265caa42baf09" "@ngrx/router-store@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-5.0.1.tgz#db872327bb958a2ebf296734c97de68672ec628a" + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-5.2.0.tgz#bf4b174ce19a36eba8211fc1ddeaf1e35ae74368" "@ngrx/store-devtools@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-5.1.0.tgz#7df8a6da652cc792000ad058ca4072a32e3629b1" + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-5.2.0.tgz#2fff916a9aa349375826772b359dbb64b9e5d622" "@ngrx/store@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-5.1.0.tgz#d957131e62041deede043524fd300db9fa835d68" + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-5.2.0.tgz#627ed74c9cd95462930485d912a557117b23903e" "@ngtools/webpack@^1.10.0": version "1.10.0" @@ -437,16 +437,16 @@ angular-idle-preload@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/angular-idle-preload/-/angular-idle-preload-2.0.4.tgz#7b177c0f52918c090e5c345480b922297cd59a0d" +angular-sortablejs@^2.5.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/angular-sortablejs/-/angular-sortablejs-2.5.2.tgz#ffd651e47cc93a191db4c023f617db3789fd9af5" + angular2-moment@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/angular2-moment/-/angular2-moment-1.9.0.tgz#d198a4d9bc825f61de19106ac7ea07a78569f5a1" dependencies: moment "^2.19.3" -angular-sortablejs@^2.5.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/angular-sortablejs/-/angular-sortablejs-2.5.2.tgz#ffd651e47cc93a191db4c023f617db3789fd9af5" - angular2-template-loader@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/angular2-template-loader/-/angular2-template-loader-0.6.2.tgz#c0d44e90fff0fac95e8b23f043acda7fd1c51d7c" @@ -1963,14 +1963,14 @@ copy-webpack-plugin@^4.4.1: p-limit "^1.0.0" serialize-javascript "^1.4.0" -core-js@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" - core-js@^2.2.0, core-js@^2.4.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" +core-js@^2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + core-js@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" From e96eeeaa835fd9197d6d88d8543e9856fd3cb70b Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 18 Sep 2018 16:29:55 +0200 Subject: [PATCH 14/22] fixed redirect after login --- .../search-filters.component.ts | 1 - src/app/core/auth/auth.service.ts | 16 +++++++------ src/app/core/auth/server-auth.service.ts | 4 +++- src/app/header/header.component.spec.ts | 6 +++-- .../auth-nav-menu.component.spec.ts | 4 +++- .../auth-nav-menu/auth-nav-menu.component.ts | 23 +++++++++++++++---- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index f4b63c332f..1da4e5860d 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -56,7 +56,6 @@ export class SearchFiltersComponent { * @returns {Observable} Emits true whenever a given filter config should be shown */ isActive(filter: SearchFilterConfig): Observable { - // console.log(filter.name); return this.filterService.getSelectedValuesForFilter(filter) .flatMap((isActive) => { if (isNotEmpty(isActive)) { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f8fc863ee9..f69dd2b58f 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -54,7 +54,7 @@ export class AuthService { protected storage: CookieService, protected store: Store, protected rdbService: RemoteDataBuildService - ) { + ) { this.store.select(isAuthenticated) .startWith(false) .subscribe((authenticated: boolean) => this._authenticated = authenticated); @@ -107,7 +107,7 @@ export class AuthService { if (status.authenticated) { return status; } else { - Observable.throw(new Error('Invalid email or password')); + throw(new Error('Invalid email or password')); } }) @@ -140,7 +140,7 @@ export class AuthService { const person$ = this.rdbService.buildSingle(status.eperson.toString()); return person$.pipe(map((eperson) => eperson.payload)); } else { - Observable.throw(new Error('Not authenticated')); + throw(new Error('Not authenticated')); } })) } @@ -216,7 +216,7 @@ export class AuthService { // Send a request that sign end the session let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); - const options: HttpOptions = Object.create({headers, responseType: 'text'}); + const options: HttpOptions = Object.create({ headers, responseType: 'text' }); return this.authRequestService.getRequest('logout', options) .map((status: AuthStatus) => { if (!status.authenticated) { @@ -225,7 +225,6 @@ export class AuthService { throw(new Error('auth.errors.invalid-user')); } }) - } /** @@ -246,6 +245,7 @@ export class AuthService { public getToken(): AuthTokenInfo { let token: AuthTokenInfo; this.store.select(getAuthenticationToken) + .first() .subscribe((authTokenInfo: AuthTokenInfo) => { // Retrieve authentication token info and check if is valid token = authTokenInfo || null; @@ -291,7 +291,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; // Save cookie with the token return this.storage.set(TOKENITEM, token, options); @@ -349,8 +349,10 @@ export class AuthService { this.router.navigated = false; const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); + this._window.nativeWindow.location.href = url; } else { this.router.navigate(['/']); + this._window.nativeWindow.location.href = '/'; } }) @@ -386,7 +388,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; this.storage.set(REDIRECT_COOKIE, url, options); this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 8d1bf1673c..18eba2e92d 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -44,7 +44,7 @@ export class ServerAuthService extends AuthService { // person$.subscribe(() => console.log('test')); return person$.pipe(map((eperson) => eperson.payload)); } else { - Observable.throw(new Error('Not authenticated')); + throw(new Error('Not authenticated')); } })) } @@ -71,8 +71,10 @@ export class ServerAuthService extends AuthService { this.router.navigated = false; const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); + this._window.nativeWindow.location.href = url; } else { this.router.navigate(['/']); + this._window.nativeWindow.location.href = '/'; } }) diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts index 87fa2995d6..9d0dd04e40 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -19,6 +19,7 @@ import { HostWindowServiceStub } from '../shared/testing/host-window-service-stu import { RouterStub } from '../shared/testing/router-stub'; import { Router } from '@angular/router'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; let comp: HeaderComponent; let fixture: ComponentFixture; @@ -35,11 +36,12 @@ describe('HeaderComponent', () => { NgbCollapseModule.forRoot(), NoopAnimationsModule, ReactiveFormsModule], - declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent], + declarations: [HeaderComponent], providers: [ { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: Router, useClass: RouterStub }, - ] + ], + schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); // compile template and css })); diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 1c59c02346..a036590ed5 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -13,7 +13,7 @@ import { HostWindowServiceStub } from '../testing/host-window-service-stub'; import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; -import { EPerson } from '../../core/eperson/models/eperson.model'; +import { AuthService } from '../../core/auth/auth.service'; describe('AuthNavMenuComponent', () => { @@ -54,6 +54,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ {provide: HostWindowService, useValue: window}, + {provide: AuthService, useValue: {setRedirectUrl: () => {}}} ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -223,6 +224,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ {provide: HostWindowService, useValue: window}, + {provide: AuthService, useValue: {setRedirectUrl: () => {}}} ], schemas: [ CUSTOM_ELEMENTS_SCHEMA diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index 887729a7af..c657071987 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -7,10 +7,14 @@ import { fadeInOut, fadeOut } from '../animations/fade'; import { HostWindowService } from '../host-window.service'; import { AppState, routerStateSelector } from '../../app.reducer'; import { isNotUndefined } from '../empty.util'; -import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; +import { + getAuthenticatedUser, + isAuthenticated, + isAuthenticationLoading +} from '../../core/auth/selectors'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; -import { RemoteData } from '../../core/data/remote-data'; +import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; +import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'ds-auth-nav-menu', @@ -37,8 +41,12 @@ export class AuthNavMenuComponent implements OnInit { public user: Observable; + public sub: Subscription; + constructor(private store: Store, - private windowService: HostWindowService) { + private windowService: HostWindowService, + private authService: AuthService + ) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -54,7 +62,12 @@ export class AuthNavMenuComponent implements OnInit { this.showAuth = this.store.select(routerStateSelector) .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)) .map((router: RouterReducerState) => { - return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); + const url = router.state.url; + const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); + if (show) { + this.authService.setRedirectUrl(url); + } + return show; }); } } From a2bcdfbea9104a57a51a852bfc1cda005f84edca Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 20 Sep 2018 15:24:16 +0200 Subject: [PATCH 15/22] disabled reloads --- src/app/core/auth/auth.service.ts | 7 ++++--- src/app/core/auth/server-auth.service.ts | 2 -- src/app/shared/auth-nav-menu/auth-nav-menu.component.ts | 8 ++------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f69dd2b58f..a7a1237e1c 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -245,7 +245,6 @@ export class AuthService { public getToken(): AuthTokenInfo { let token: AuthTokenInfo; this.store.select(getAuthenticationToken) - .first() .subscribe((authTokenInfo: AuthTokenInfo) => { // Retrieve authentication token info and check if is valid token = authTokenInfo || null; @@ -349,10 +348,12 @@ export class AuthService { this.router.navigated = false; const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); - this._window.nativeWindow.location.href = url; + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for */ + // this._window.nativeWindow.location.href = url; } else { this.router.navigate(['/']); - this._window.nativeWindow.location.href = '/'; + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for */ + // this._window.nativeWindow.location.href = '/'; } }) diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 18eba2e92d..9030443a1e 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -71,10 +71,8 @@ export class ServerAuthService extends AuthService { this.router.navigated = false; const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); - this._window.nativeWindow.location.href = url; } else { this.router.navigate(['/']); - this._window.nativeWindow.location.href = '/'; } }) diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index c657071987..f7e1b03c88 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -62,12 +62,8 @@ export class AuthNavMenuComponent implements OnInit { this.showAuth = this.store.select(routerStateSelector) .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)) .map((router: RouterReducerState) => { - const url = router.state.url; - const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); - if (show) { - this.authService.setRedirectUrl(url); - } - return show; + return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); + }); } } From 7db30e88090104bde234663711eca76a8efd7c05 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 20 Sep 2018 15:37:54 +0200 Subject: [PATCH 16/22] fixed tslint error --- .../shared/auth-nav-menu/auth-nav-menu.component.spec.ts | 4 ++-- src/app/shared/auth-nav-menu/auth-nav-menu.component.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index a036590ed5..e1a82f4a33 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -54,7 +54,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ {provide: HostWindowService, useValue: window}, - {provide: AuthService, useValue: {setRedirectUrl: () => {}}} + {provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}} ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -224,7 +224,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ {provide: HostWindowService, useValue: window}, - {provide: AuthService, useValue: {setRedirectUrl: () => {}}} + {provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}} ], schemas: [ CUSTOM_ELEMENTS_SCHEMA diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index f7e1b03c88..c657071987 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -62,8 +62,12 @@ export class AuthNavMenuComponent implements OnInit { this.showAuth = this.store.select(routerStateSelector) .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)) .map((router: RouterReducerState) => { - return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); - + const url = router.state.url; + const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); + if (show) { + this.authService.setRedirectUrl(url); + } + return show; }); } } From 9029231396601da748ed1ee299357765d9099be9 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 21 Sep 2018 12:02:06 -0500 Subject: [PATCH 17/22] Update version of Bootstrap per CVE-2018-14041 --- package.json | 6 +++--- yarn.lock | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0936b27ea4..b058730545 100644 --- a/package.json +++ b/package.json @@ -90,12 +90,12 @@ "@ngx-translate/http-loader": "2.0.1", "@nicky-lenaers/ngx-scroll-to": "^0.6.0", "angular-idle-preload": "2.0.4", - "angular2-moment": "^1.9.0", "angular-sortablejs": "^2.5.0", + "angular2-moment": "^1.9.0", "angular2-text-mask": "8.0.4", "angulartics2": "^5.2.0", "body-parser": "1.18.2", - "bootstrap": "4.1.1", + "bootstrap": "4.1.3", "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", @@ -112,8 +112,8 @@ "methods": "1.1.2", "moment": "^2.22.1", "morgan": "1.9.0", - "ng2-nouislider": "^1.7.11", "ng2-file-upload": "1.2.1", + "ng2-nouislider": "^1.7.11", "ngx-infinite-scroll": "0.8.2", "ngx-pagination": "3.0.3", "nouislider": "^11.0.0", diff --git a/yarn.lock b/yarn.lock index 344093d446..95cf41ac55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -437,16 +437,16 @@ angular-idle-preload@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/angular-idle-preload/-/angular-idle-preload-2.0.4.tgz#7b177c0f52918c090e5c345480b922297cd59a0d" +angular-sortablejs@^2.5.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/angular-sortablejs/-/angular-sortablejs-2.5.2.tgz#ffd651e47cc93a191db4c023f617db3789fd9af5" + angular2-moment@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/angular2-moment/-/angular2-moment-1.9.0.tgz#d198a4d9bc825f61de19106ac7ea07a78569f5a1" dependencies: moment "^2.19.3" -angular-sortablejs@^2.5.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/angular-sortablejs/-/angular-sortablejs-2.5.2.tgz#ffd651e47cc93a191db4c023f617db3789fd9af5" - angular2-template-loader@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/angular2-template-loader/-/angular2-template-loader-0.6.2.tgz#c0d44e90fff0fac95e8b23f043acda7fd1c51d7c" @@ -1070,9 +1070,9 @@ boom@5.x.x: dependencies: hoek "4.x.x" -bootstrap@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb" +bootstrap@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be" boxen@^1.2.1: version "1.3.0" From bfc70f4f881d22366410df0e8f47ed629adb8d2d Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 27 Sep 2018 10:37:40 +0200 Subject: [PATCH 18/22] Added links to issues --- src/app/core/auth/auth.service.ts | 6 ++++-- src/app/core/data/base-response-parsing.service.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a7a1237e1c..5f113b0262 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -137,6 +137,8 @@ export class AuthService { if (status.authenticated) { // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... + // Review when https://jira.duraspace.org/browse/DS-4006 is fixed + // See https://github.com/DSpace/dspace-angular/issues/292 const person$ = this.rdbService.buildSingle(status.eperson.toString()); return person$.pipe(map((eperson) => eperson.payload)); } else { @@ -348,11 +350,11 @@ export class AuthService { this.router.navigated = false; const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); - /* TODO Reenable hard redirect when REST API can handle x-forwarded-for */ + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ // this._window.nativeWindow.location.href = url; } else { this.router.navigate(['/']); - /* TODO Reenable hard redirect when REST API can handle x-forwarded-for */ + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ // this._window.nativeWindow.location.href = '/'; } }) diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index bdd18f8a61..fdf5b4eb97 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -150,7 +150,8 @@ export abstract class BaseResponseParsingService { return obj[keys[0]]; } - /* TODO remove when REST response for epersons is fixed */ + // TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed + // See https://github.com/DSpace/dspace-angular/issues/292 private fixBadEPersonRestResponse(obj: any): any { if (obj.type === ResourceType.EPerson) { const groups = obj.groups; From 031c4c0cf5565d1da0c84c7af92182465434a366 Mon Sep 17 00:00:00 2001 From: Bram Luyten Date: Thu, 4 Oct 2018 07:03:49 +0200 Subject: [PATCH 19/22] Dutch translation --- resources/i18n/nl.json | 277 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 resources/i18n/nl.json diff --git a/resources/i18n/nl.json b/resources/i18n/nl.json new file mode 100644 index 0000000000..6c3b1fe401 --- /dev/null +++ b/resources/i18n/nl.json @@ -0,0 +1,277 @@ +{ + "footer": { + "copyright": "copyright © 2002-{{ year }}", + "link.dspace": "DSpace software", + "link.duraspace": "DuraSpace" + }, + "collection": { + "page": { + "news": "Nieuws", + "license": "Licentie", + "browse": { + "recent": { + "head": "Recent toegevoegd" + } + } + } + }, + "community": { + "page": { + "news": "Nieuws", + "license": "Licentie" + }, + "sub-collection-list": { + "head": "Collecties in deze Community" + } + }, + "item": { + "page": { + "author": "Auteur", + "abstract": "Abstract", + "date": "Datum", + "uri": "URI", + "files": "Bestanden", + "collections": "Collecties", + "filesection": { + "download": "Download", + "name": "Naam:", + "format": "Formaat:", + "size": "Grootte:", + "description": "Beschrijving:" + }, + "link": { + "simple": "Eenvoudige item weergave", + "full": "Volledige item weergave" + } + } + }, + "nav": { + "home": "Home", + "login": "Log In", + "logout": "Log Uit" + }, + "pagination": { + "results-per-page": "Resultaten per pagina", + "sort-direction": "Sorteer mogelijkheden", + "showing": { + "label": "Getoonde items ", + "detail": "{{ range }} tot {{ total }}" + } + }, + "sorting": { + "score": { + "DESC": "Relevantie" + }, + "dc.title": { + "ASC": "Oplopend op titel", + "DESC": "Aflopend op titel" + } + }, + "title": "DSpace", + "404": { + "help": "De pagina die u zoekt kan niet gevonden worden. De pagina werd mogelijk verplaatst of verwijderd. U kan onderstaande knop gebruiken om terug naar de homepagina te gaan. ", + "page-not-found": "Pagina niet gevonden", + "link": { + "home-page": "Terug naar de homepagina" + } + }, + "home": { + "title": "DSpace Angular :: Home", + "description": "", + "top-level-communities": { + "head": "Communities in DSpace", + "help": "Selecteer een community om diens collecties te verkennen." + } + }, + "search": { + "title": "DSpace Angular :: Zoek", + "description": "", + "form": { + "search": "Zoek", + "search_dspace": "Zoek in DSpace" + }, + "results": { + "head": "Zoekresultaten", + "no-results": "Er waren geen resultaten voor deze zoekopdracht" + }, + "sidebar": { + "close": "Terug naar de resultaten", + "open": "Zoek Tools", + "results": "resultaten", + "filters": { + "title": "Filters" + }, + "settings": { + "title": "Instellingen", + "sort-by": "Sorteer volgens", + "rpp": "Resultaten per pagina" + } + }, + "view-switch": { + "show-list": "Toon als lijst", + "show-grid": "Toon in raster" + }, + "filters": { + "head": "Filters", + "reset": "Filters verwijderen", + "applied": { + "f.author": "Auteur", + "f.dateIssued.min": "Start datum", + "f.dateIssued.max": "Eind datum", + "f.subject": "Sleutelwoord", + "f.has_content_in_original_bundle": "Heeft bestanden" + }, + "filter": { + "show-more": "Toon meer", + "show-less": "Inklappen", + "author": { + "placeholder": "Auteursnaam", + "head": "Auteur" + }, + "scope": { + "placeholder": "Bereik filter", + "head": "Bereik" + }, + "subject": { + "placeholder": "Onderwerp", + "head": "Onderwerp" + }, + "dateIssued": { + "max": { + "placeholder": "Vroegste Datum" + }, + "min": { + "placeholder": "Laatste Datum" + }, + "head": "Datum" + }, + "has_content_in_original_bundle": { + "head": "Heeft bestanden" + } + } + } + }, + "browse": { + "title": "Verken {{ collection }} volgens {{ field }} {{ value }}" + }, + "admin": { + "registries": { + "metadata": { + "title": "DSpace Angular :: Metadata Register", + "head": "Metadata Register", + "description": "Het metadata register omvat de lijst van alle metadata velden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadata schema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.", + "schemas": { + "table": { + "id": "ID", + "namespace": "Naamruimte", + "name": "Naam" + }, + "no-items": "Er kunnen geen metadata schema's getoond worden." + } + }, + "schema": { + "title": "DSpace Angular :: Metadata Schema Register", + "head": "Metadata Schema", + "description": "Dit is het metadata schema voor \"{{namespace}}\".", + "fields": { + "head": "Schema metadata velden", + "table": { + "field": "Veld", + "scopenote": "Opmerking over bereik" + }, + "no-items": "Er kunnen geen metadata velden getoond worden." + } + }, + "bitstream-formats": { + "title": "DSpace Angular :: Bitstream Formaat Register", + "head": "Bitstream Formaat Register", + "description": "Deze lijst van Bitstream formaten biedt informatie over de formaten die in deze repository zijn toegelaten en op welke manier ze ondersteund worden. De term Bitstream wordt in DSpace gebruikt om een bestand aan te duiden dat samen met metadata onderdeel uitmaakt van een item. De naam bitstream duidt op het feit dat het bestand achterliggend wordt opgeslaan zonder bestandsextensie.", + "formats": { + "table": { + "name": "Naam", + "mimetype": "MIME Type", + "supportLevel": { + "head": "Ondersteuning", + "0": "Onbekend", + "1": "Gekend", + "2": "Ondersteund" + }, + "internal": "intern" + }, + "no-items": "Er kunnen geen bitstream formaten getoond worden." + } + } + } + }, + "loading": { + "default": "Laden...", + "top-level-communities": "Inladen van de Communities op het hoogste niveau...", + "community": "Community wordt ingeladen...", + "collection": "Collectie wordt ingeladen...", + "sub-collections": "De sub-collecties worden ingeladen...", + "recent-submissions": "Recent toegevoegde items worden ingeladen...", + "item": "Item wordt ingeladen...", + "objects": "Laden...", + "search-results": "Zoekresultaten worden ingeladen...", + "browse-by": "Items worden ingeladen..." + }, + "error": { + "default": "Fout", + "top-level-communities": "Fout bij het inladen van communities op het hoogste niveau", + "community": "Fout bij het ophalen van een community", + "collection": "Fout bij het ophalen van een collectie", + "sub-collections": "Fout bij het ophalen van sub-collecties", + "recent-submissions": "Fout bij het ophalen van recent toegevoegde items", + "item": "Fout bij het ophalen van items", + "objects": "Fout bij het ophalen van objecten", + "search-results": "Fout bij het ophalen van zoekresultaten", + "browse-by": "Fout bij het ophalen van items", + "validation": { + "pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.", + "license": { + "notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kan dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoer licentie." + } + } + }, + "form": { + "submit": "Verstuur", + "cancel": "Annuleer", + "search": "Zoek", + "remove": "Verwijder", + "first-name": "Voornaam", + "last-name": "Achternaam", + "loading": "Inladen...", + "no-results": "Geen resultaten gevonden", + "no-value": "Geen waarde ingevoerd", + "group-collapse": "Inklappen", + "group-expand": "Uitklappen", + "group-collapse-help": "Klik hier op in te klappen", + "group-expand-help": "Klik hier om uit te klappen en om meer onderdelen toe te voegen" + }, + "login": { + "title": "Aanmelden", + "form": { + "header": "Gelieve in te loggen in DSpace", + "email": "Email adres", + "forgot-password": "Bent u uw wachtwoord vergeten?", + "new-user": "Nieuwe gebruiker? Gelieve u hier te registreren", + "password": "Wachtwoord", + "submit": "Aanmelden" + } + }, + "logout": { + "title": "Afmelden", + "form": { + "header": "Afmelden in DSpace", + "submit": "Afmelden" + } + }, + "auth": { + "messages": { + "expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden." + }, + "errors": { + "invalid-user": "Ongeldig email adres of wachtwoord." + } + } +} From b2fef4d60a4036000baa6f83b1651012cb6482f8 Mon Sep 17 00:00:00 2001 From: helix84 Date: Thu, 4 Oct 2018 09:22:46 +0200 Subject: [PATCH 20/22] Create cs.json --- resources/i18n/cs.json | 277 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 resources/i18n/cs.json diff --git a/resources/i18n/cs.json b/resources/i18n/cs.json new file mode 100644 index 0000000000..1fdd02401b --- /dev/null +++ b/resources/i18n/cs.json @@ -0,0 +1,277 @@ +{ + "footer": { + "copyright": "copyright © 2002-{{ year }}", + "link.dspace": "software DSpace", + "link.duraspace": "DuraSpace" + }, + "collection": { + "page": { + "news": "Novinky", + "license": "Licence", + "browse": { + "recent": { + "head": "Poslední příspěvky" + } + } + } + }, + "community": { + "page": { + "news": "Novinky", + "license": "Licence" + }, + "sub-collection-list": { + "head": "Kolekce v této komunitě" + } + }, + "item": { + "page": { + "author": "Autor", + "abstract": "Abstract", + "date": "Datum", + "uri": "URI", + "files": "Soubory", + "collections": "Kolekce", + "filesection": { + "download": "Stáhnout", + "name": "Název:", + "format": "Formát:", + "size": "Velikost:", + "description": "Popis:" + }, + "link": { + "simple": "Minimální záznam", + "full": "Úplný záznam" + } + } + }, + "nav": { + "home": "Domů", + "login": "Přihlásit se", + "logout": "Odhlásit se" + }, + "pagination": { + "results-per-page": "Výsledků na stránku", + "sort-direction": "Seřazení", + "showing": { + "label": "Zobrazují se záznamy ", + "detail": "{{ range }} z {{ total }}" + } + }, + "sorting": { + "score": { + "DESC": "Relevance" + }, + "dc.title": { + "ASC": "Název vzestupně", + "DESC": "Název sestupně" + } + }, + "title": "DSpace", + "404": { + "help": "Nepodařilo se najít stránku, kterou hledáte. Je možné, že stránka byla přesunuta nebo smazána. Pomocí tlačítka níže můžete přejít na domovskou stránku. ", + "page-not-found": "stránka nenalezena", + "link": { + "home-page": "Přejít na domovskou stránku" + } + }, + "home": { + "title": "DSpace Angular :: Domů", + "description": "", + "top-level-communities": { + "head": "Komunity v DSpace", + "help": "Vybráním komunity můžete prohlížet její kolekce." + } + }, + "search": { + "title": "DSpace Angular :: Hledat", + "description": "", + "form": { + "search": "Hledat", + "search_dspace": "Hledat v DSpace" + }, + "results": { + "head": "Výsledky hledání", + "no-results": "Nebyli nalezeny žádné výsledky" + }, + "sidebar": { + "close": "Zpět na výsledky", + "open": "Vyhledávací nástroje", + "results": "výsledky", + "filters": { + "title": "Filtry" + }, + "settings": { + "title": "Nastavení", + "sort-by": "Řadit dle", + "rpp": "Výsledků na stránku" + } + }, + "view-switch": { + "show-list": "Zobrazit seznam", + "show-grid": "Zobrazit mřížku" + }, + "filters": { + "head": "Filtry", + "reset": "Obnovit filtry", + "applied": { + "f.author": "Autor", + "f.dateIssued.min": "Od data", + "f.dateIssued.max": "Do data", + "f.subject": "Předmět", + "f.has_content_in_original_bundle": "Má soubory" + }, + "filter": { + "show-more": "Zobrazit více", + "show-less": "Sbalit", + "author": { + "placeholder": "Jméno autora", + "head": "Autor" + }, + "scope": { + "placeholder": "Filtr rozsahu", + "head": "Rozsah" + }, + "subject": { + "placeholder": "Předmět", + "head": "Předmět" + }, + "dateIssued": { + "max": { + "placeholder": "Datum od" + }, + "min": { + "placeholder": "Datum do" + }, + "head": "Datum" + }, + "has_content_in_original_bundle": { + "head": "Má soubory" + } + } + } + }, + "browse": { + "title": "Prohlížíte {{ collection }} dle {{ field }} {{ value }}" + }, + "admin": { + "registries": { + "metadata": { + "title": "DSpace Angular :: Registr metadat", + "head": "Registr metadat", + "description": "Registr metadat je seznam všech metadatových polí dostupných v repozitáři. Tyto pole mohou být rozdělena do více schémat. DSpace však vyžaduje použití schématu kvalifikový Dublin Core.", + "schemas": { + "table": { + "id": "ID", + "namespace": "Jmenný prostor", + "name": "Název" + }, + "no-items": "Žádná schémata metadat." + } + }, + "schema": { + "title": "DSpace Angular :: Registr schémat metadat", + "head": "Metadata Schema", + "description": "Toto je schéma metadat pro „{{namespace}}“.", + "fields": { + "head": "Pole schématu metadat", + "table": { + "field": "Pole", + "scopenote": "Poznámka o rozsahu" + }, + "no-items": "Žádná metadatová pole." + } + }, + "bitstream-formats": { + "title": "DSpace Angular :: Registr formátů souborů", + "head": "Registr formátů souborů", + "description": "Tento seznam formátů souborů poskytuje informace o známých formátech a o úrovni jejich podpory.", + "formats": { + "table": { + "name": "Název", + "mimetype": "Typ MIME", + "supportLevel": { + "head": "Úroveň podpory", + "0": "Neznámá", + "1": "Známá", + "2": "Podpora" + }, + "internal": "interní" + }, + "no-items": "Žádné formáty souborů." + } + } + } + }, + "loading": { + "default": "Načítá se...", + "top-level-communities": "Načítají se komunity nejvyšší úrovně...", + "community": "Načítá se komunita...", + "collection": "Načítá se kolekce...", + "sub-collections": "Načítají se subkolekce...", + "recent-submissions": "Načítají se poslední příspěvky...", + "item": "Načítá se záznam...", + "objects": "Načítá se...", + "search-results": "Načítají se výsledky hledání...", + "browse-by": "Načítají se záznamy..." + }, + "error": { + "default": "Chyba", + "top-level-communities": "Chyba během stahování komunit nejvyšší úrovně", + "community": "Chyba během stahování komunity", + "collection": "Chyba během stahování kolekce", + "sub-collections": "Chyba během stahování subkolekcí", + "recent-submissions": "Chyba během stahování posledních příspěvků", + "item": "Chyba během stahování záznamu", + "objects": "Chyba během stahování objektů", + "search-results": "Chyba během stahování výsledků hledání", + "browse-by": "Chyba během stahování záznamů", + "validation": { + "pattern": "Tento vstup je omezen dle vzoru: {{ pattern }}.", + "license": { + "notgranted": "Pro dokončení zaslání Musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat." + } + } + }, + "form": { + "submit": "Odeslat", + "cancel": "Zrušit", + "search": "Hledat", + "remove": "Smazat", + "first-name": "Křestní jméno", + "last-name": "Příjmení", + "loading": "Načítá se...", + "no-results": "Nebyli nalezeny žádné výsledky", + "no-value": "Nebyla zadána hodnota", + "group-collapse": "Sbalit", + "group-expand": "Rozbalit", + "group-collapse-help": "Kliknutím sem sbalíte", + "group-expand-help": "Kliknutím sem rozbalíte a přidáte další prvky" + }, + "login": { + "title": "Přihlásit se", + "form": { + "header": "Prosím, přihlaste se do DSpace", + "email": "E-mailová adresa", + "forgot-password": "Zapomněli jste své heslo?", + "new-user": "Nový uživatel? Zaregistrujte se kliknutím sem.", + "password": "Heslo", + "submit": "Přihlásit se" + } + }, + "logout": { + "title": "Odhlásit se", + "form": { + "header": "Odhlásit se z DSpace", + "submit": "Odhlásit se" + } + }, + "auth": { + "messages": { + "expired": "Vaše relace vypršela. Prosím, znova se přihlaste." + }, + "errors": { + "invalid-user": "Neplatná e-mailová adresa nebo heslo." + } + } +} From a83c6c05dfc479ebead627ec449e1dcf10df498f Mon Sep 17 00:00:00 2001 From: helix84 Date: Thu, 4 Oct 2018 09:25:38 +0200 Subject: [PATCH 21/22] minor fixes in en.json --- resources/i18n/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index c7cb9a5ba7..b6a23068d7 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -205,7 +205,7 @@ }, "loading": { "default": "Loading...", - "top-level-communities": "Loading top level communities...", + "top-level-communities": "Loading top-level communities...", "community": "Loading community...", "collection": "Loading collection...", "sub-collections": "Loading sub-collections...", @@ -217,7 +217,7 @@ }, "error": { "default": "Error", - "top-level-communities": "Error fetching top level communities", + "top-level-communities": "Error fetching top-level communities", "community": "Error fetching community", "collection": "Error fetching collection", "sub-collections": "Error fetching sub-collections", @@ -246,7 +246,7 @@ "group-collapse": "Collapse", "group-expand": "Expand", "group-collapse-help": "Click here to collapse", - "group-expand-help": "Click here to expand and add more element" + "group-expand-help": "Click here to expand and add more elements" }, "login": { "title": "Login", @@ -271,7 +271,7 @@ "expired": "Your session has expired. Please log in again." }, "errors": { - "invalid-user": "Invalid email or password." + "invalid-user": "Invalid email address or password." } } } From c8ab655b4cea5291e45fb2def5b1c516ee2c251f Mon Sep 17 00:00:00 2001 From: cjuergen Date: Mon, 8 Oct 2018 10:49:59 +0200 Subject: [PATCH 22/22] Initial German Translation --- resources/i18n/de.json | 277 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 resources/i18n/de.json diff --git a/resources/i18n/de.json b/resources/i18n/de.json new file mode 100644 index 0000000000..d6b02ff533 --- /dev/null +++ b/resources/i18n/de.json @@ -0,0 +1,277 @@ +{ + "footer": { + "copyright": "Copyright © 2002-{{ year }}", + "link.dspace": "DSpace Software", + "link.duraspace": "DuraSpace" + }, + "collection": { + "page": { + "news": "Neuigkeiten", + "license": "Lizenz", + "browse": { + "recent": { + "head": "Aktuellste Veröffentlichungen" + } + } + } + }, + "community": { + "page": { + "news": "Neuigkeiten", + "license": "Lizenz" + }, + "sub-collection-list": { + "head": "Sammlungen in diesem Bereich" + } + }, + "item": { + "page": { + "author": "Autor", + "abstract": "Kurzfassung", + "date": "Datum", + "uri": "URI", + "files": "Dateien", + "collections": "Sammlungen", + "filesection": { + "download": "Herunterladen", + "name": "Name:", + "format": "Format:", + "size": "Größe:", + "description": "Beschreibung:" + }, + "link": { + "simple": "Kurzanzeige", + "full": "Vollanzeige" + } + } + }, + "nav": { + "home": "Zur Startseite", + "login": "Anmelden", + "logout": "Abmelden" + }, + "pagination": { + "results-per-page": "Ergebnisse pro Seite", + "sort-direction": "Sortiermöglichkeiten", + "showing": { + "label": "Anzeige der Treffer ", + "detail": "{{ range }} bis {{ total }}" + } + }, + "sorting": { + "score": { + "DESC": "Relevanz" + }, + "dc.title": { + "ASC": "Titel aufsteigend", + "DESC": "Titel absteigend" + } + }, + "title": "DSpace", + "404": { + "help": "Die Seite, die Sie aufrufen wollten, konnte nicht gefunden werden. Sie könnte verschoben oder gelöscht worden sein. Mit dem Link unten kommen Sie zurück zur Startseite. ", + "page-not-found": "Seite nicht gefunden", + "link": { + "home-page": "Zurück zur Startseite" + } + }, + "home": { + "title": "DSpace Angular :: Startseite", + "description": "", + "top-level-communities": { + "head": "Bereiche in DSpace", + "help": "Wählen Sie einen Bereich, um seine Sammlungen einzusehen." + } + }, + "search": { + "title": "DSpace Angular :: Suche", + "description": "", + "form": { + "search": "Suche", + "search_dspace": "DSpace durchsuchen" + }, + "results": { + "head": "Suchergebnisse", + "no-results": "Zu dieser Suche gibt es keine Treffer." + }, + "sidebar": { + "close": "Zurück zu den Ergebnissen", + "open": "Suchwerkzeuge", + "results": "Ergebnisse", + "filters": { + "title": "Filter" + }, + "settings": { + "title": "Einstellungen", + "sort-by": "Sortiere nach", + "rpp": "Treffer pro Seite" + } + }, + "view-switch": { + "show-list": "Zeige als Liste", + "show-grid": "Zeige als Raster" + }, + "filters": { + "head": "Filter", + "reset": "Filter zurücksetzen", + "applied": { + "f.author": "Autor", + "f.dateIssued.min": "Anfangsdatum", + "f.dateIssued.max": "Enddatum", + "f.subject": "Thema", + "f.has_content_in_original_bundle": "Besitzt Dateien" + }, + "filter": { + "show-more": "Zeige mehr", + "show-less": "Zeige weniger", + "author": { + "placeholder": "Autor", + "head": "Autor" + }, + "scope": { + "placeholder": "Bereichsfilter", + "head": "Bereich" + }, + "subject": { + "placeholder": "Schlagwort", + "head": "Schlagwort" + }, + "dateIssued": { + "max": { + "placeholder": "Frühestes Datum" + }, + "min": { + "placeholder": "Ältestes Datum" + }, + "head": "Datum" + }, + "has_content_in_original_bundle": { + "head": "Besitzt Dateien" + } + } + } + }, + "browse": { + "title": "Anzeige {{ collection }} nach {{ field }} {{ value }}" + }, + "admin": { + "registries": { + "metadata": { + "title": "DSpace Angular :: Metadatenreferenzliste", + "head": "Metadatenreferenzliste", + "description": "Die Metadatenreferenzliste beinhaltet alle Metadatenfelder, die zur Verfügung stehen. Die Felder können in unterschiedlichen Schemata enthalten sein. Nichtsdestotrotz benötigt DSpace mindestens qualifiziertes Dublin Core.", + "schemas": { + "table": { + "id": "ID", + "namespace": "Namensraum", + "name": "Name" + }, + "no-items": "Es gbit keine Metadatenschemata." + } + }, + "schema": { + "title": "DSpace Angular :: Referenzliste der Metadatenschemata", + "head": "Metadatenschemata", + "description": "Dies ist das Metadatenschema für \"{{namespace}}\".", + "fields": { + "head": "Felder in diesem Schema", + "table": { + "field": "Feld", + "scopenote": "Gültigkeitsbereich" + }, + "no-items": "Es gibt keine Felder in diesem Schema." + } + }, + "bitstream-formats": { + "title": "DSpace Angular :: Referenzliste der Dateiformate", + "head": "Referenzliste der Dateiformate", + "description": "Diese Liste enhtält die in diesem Repositorium zulässigen Dateiformate und den jeweiligen Unterstützungsgrad.", + "formats": { + "table": { + "name": "Name", + "mimetype": "MIME Type", + "supportLevel": { + "head": "Unterstützungsgrad", + "0": "Unbekannt", + "1": "Bekannt", + "2": "Unterstützt" + }, + "internal": "intern" + }, + "no-items": "Es gibt keine Formate in dieser Referenzliste." + } + } + } + }, + "loading": { + "default": "Am Laden ...", + "top-level-communities": "Die Hauptbereiche werden geladen ...", + "community": "Der Bereich wird geladen ...", + "collection": "Die Sammlung wird geladen ...", + "sub-collections": "Die untergeordneten Sammlungen werden geladen ...", + "recent-submissions": "Die aktuellsten Veröffentlichungen werden geladen ...", + "item": "Die Ressource wird geladen ...", + "objects": "Am Laden ...", + "search-results": "Die Suchergebnisse werden geladen ...", + "browse-by": "Die Ressourcen werden geladen ..." + }, + "error": { + "default": "Fehler", + "top-level-communities": "Fehler beim Laden der Hauptbereiche.", + "community": "Fehler beim Laden des Bereiches.", + "collection": "Fehler beim Laden der Sammlung.", + "sub-collections": "Fehler beim Laden der untergeordneten Sammlungen.", + "recent-submissions": "Fehler beim Laden der aktuellsten Veröffentlichungen.", + "item": "Fehler beim Laden der Ressource.", + "objects": "Fehler beim Laden der Objekte.", + "search-results": "Fehler beim Laden der Suchergebnisse.", + "browse-by": "Fehler beim Laden der Ressourcen", + "validation": { + "pattern": "Die Eingabe kann nur folgendes Muster haben: {{ pattern }}.", + "license": { + "notgranted": "Sie müssen der Lizenz zustimmen, um die Ressource einzureichen. Wenn dies zur Zeit nicht geht, können Sie die Einreichung speichern und später wiederaufnehmen oder löschen." + } + } + }, + "form": { + "submit": "Los", + "cancel": "Abbrechen", + "search": "Suchen", + "remove": "Löschen", + "first-name": "Vorname", + "last-name": "Nachname", + "loading": "Am Laden ...", + "no-results": "Keine Ergebnisse gefunden", + "no-value": "Kein Wert eingegeben", + "group-collapse": "Weniger", + "group-expand": "Mehr", + "group-collapse-help": "Hier klicken, um die Anzeige zu reduzieren", + "group-expand-help": "Hier klicken, um mehr Elemente anzuzeigen" + }, + "login": { + "title": "Einloggen", + "form": { + "header": "Bitte Loggen Sie sich ein.", + "email": "E-Mail-Adresse", + "forgot-password": "Haben Sie Ihr Passwort vergessen?", + "new-user": "Sind Sie neu hier? Klicken Sie hier, um sich zu registrieren.", + "password": "Passwort", + "submit": "Einloggen" + } + }, + "logout": { + "title": "Ausloggen", + "form": { + "header": "Ausloggen aus DSpace", + "submit": "Ausloggen" + } + }, + "auth": { + "messages": { + "expired": "Ihre Sitzung ist abgelaufen, bitte melden Sie sich erneut an." + }, + "errors": { + "invalid-user": "Ungültige E-Mail-Adresse oder Passwort." + } + } +}