mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Issues 252,253: Browse by title and browse by metadata (author)
This commit is contained in:
@@ -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": {
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="browse-by-author w-100 row">
|
||||
<ds-browse-by class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '"' + value + '"': ''} }}"
|
||||
[objects$]="(items$ !== undefined)? items$ : authors$"
|
||||
[currentUrl]="currentUrl"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig">
|
||||
</ds-browse-by>
|
||||
</div>
|
||||
</div>
|
@@ -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<RemoteData<PaginatedList<BrowseEntry>>>;
|
||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="browse-by-title w-100 row">
|
||||
<ds-browse-by class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
|
||||
[objects$]="items$"
|
||||
[currentUrl]="currentUrl"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig">
|
||||
</ds-browse-by>
|
||||
</div>
|
||||
</div>
|
@@ -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<RemoteData<PaginatedList<Item>>>;
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
16
src/app/+browse-by/browse-by-routing.module.ts
Normal file
16
src/app/+browse-by/browse-by-routing.module.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
27
src/app/+browse-by/browse-by.module.ts
Normal file
27
src/app/+browse-by/browse-by.module.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
@@ -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' },
|
||||
|
@@ -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<BrowseDefinition[]>) => 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<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||
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<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||
map((list: PaginatedList<BrowseEntry>) => 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<RemoteData<PaginatedList<Item>>> {
|
||||
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<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||
map((list: PaginatedList<Item>) => Object.assign(list, {
|
||||
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
||||
})),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
|
@@ -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,
|
||||
|
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal file
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
49
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
49
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
@@ -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 }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
|
@@ -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<TNormalized extends NormalizedObject, TD
|
||||
* @return { Observable<string> }
|
||||
* an Observable<string> containing the scoped URL
|
||||
*/
|
||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||
if (isEmpty(scopeID)) {
|
||||
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
|
||||
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<TNormalized extends NormalizedObject, TD
|
||||
|
||||
return Observable.merge(
|
||||
errorResponse.flatMap((response: ErrorResponse) =>
|
||||
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();
|
||||
|
@@ -20,17 +20,13 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
||||
protected abstract linkPath: string;
|
||||
protected abstract halService: HALEndpointService;
|
||||
|
||||
public abstract getScopedEndpoint(scope: string): Observable<string>
|
||||
public abstract getBrowseEndpoint(options: FindAllOptions): Observable<string>
|
||||
|
||||
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
|
||||
protected getFindAllHref(options: FindAllOptions = {}): Observable<string> {
|
||||
let result: Observable<string>;
|
||||
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<TNormalized extends NormalizedObject, TDomain>
|
||||
}
|
||||
|
||||
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
||||
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))
|
||||
|
@@ -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);
|
||||
|
@@ -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<NormalizedItem, Item> {
|
||||
@@ -30,15 +31,15 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
super();
|
||||
}
|
||||
|
||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||
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<string> {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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<ResponseParsingService> {
|
||||
return BrowseItemsResponseParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigRequest extends GetRequest {
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
|
@@ -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;
|
||||
|
@@ -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[];
|
||||
|
||||
/**
|
||||
|
@@ -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 = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
||||
|
||||
export const getBrowseDefinitionLinks = (definitionID: string) =>
|
||||
(source: Observable<RemoteData<BrowseDefinition[]>>): Observable<any> =>
|
||||
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}'`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
12
src/app/shared/browse-by/browse-by.component.html
Normal file
12
src/app/shared/browse-by/browse-by.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<ng-container *ngVar="(objects$ | async) as objects">
|
||||
<h2 class="w-100">{{title}}</h2>
|
||||
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
||||
<ds-viewable-collection
|
||||
[config]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-loading *ngIf="!objects || objects?.payload?.page.length <= 0" message="{{'loading.browse-by' | translate}}"></ds-loading>
|
||||
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
|
||||
</ng-container>
|
0
src/app/shared/browse-by/browse-by.component.scss
Normal file
0
src/app/shared/browse-by/browse-by.component.scss
Normal file
44
src/app/shared/browse-by/browse-by.component.spec.ts
Normal file
44
src/app/shared/browse-by/browse-by.component.spec.ts
Normal file
@@ -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<BrowseByComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
27
src/app/shared/browse-by/browse-by.component.ts
Normal file
27
src/app/shared/browse-by/browse-by.component.ts
Normal file
@@ -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<RemoteData<PaginatedList<ListableObject>>>;
|
||||
@Input() paginationConfig: PaginationComponentOptions;
|
||||
@Input() sortConfig: SortOptions;
|
||||
@Input() currentUrl: string;
|
||||
query: string;
|
||||
}
|
@@ -8,7 +8,7 @@
|
||||
<ds-object-grid [config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects"
|
||||
[hideGear]="true"
|
||||
[hideGear]="hideGear"
|
||||
*ngIf="getViewMode()===viewModeEnum.Grid">
|
||||
</ds-object-grid>
|
||||
|
||||
|
@@ -1,22 +1,28 @@
|
||||
<div class="card">
|
||||
<ds-truncatable [id]="object.id">
|
||||
<div class="card">
|
||||
<a [routerLink]="['/items/', object.id]" class="card-img-top">
|
||||
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
||||
</ds-grid-thumbnail>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
||||
|
||||
<a [routerLink]="['/items/', object.id]" class="card-img-top">
|
||||
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
||||
</ds-grid-thumbnail>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
</p>
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="2">
|
||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
</p>
|
||||
</ds-truncatable-part>
|
||||
|
||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="5">
|
||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
|
||||
</ds-truncatable-part>
|
||||
|
||||
<div class="text-center">
|
||||
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<div class="text-center pt-2">
|
||||
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
|
||||
{{object.value}}
|
||||
</a>
|
@@ -0,0 +1 @@
|
||||
@import '../../../../styles/variables';
|
@@ -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<BrowseEntryListElementComponent>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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<BrowseEntry> {}
|
@@ -1,18 +1,24 @@
|
||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||
{{object.findMetadata("dc.title")}}
|
||||
</a>
|
||||
<div>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-list-authors">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
<ds-truncatable [id]="object.id">
|
||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||
{{object.findMetadata("dc.title")}}
|
||||
</a>
|
||||
<div>
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="1">
|
||||
<span class="text-muted">
|
||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-list-authors">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
||||
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||
</span>
|
||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
|
||||
</div>
|
||||
</div>
|
||||
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
||||
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||
</span>
|
||||
</ds-truncatable-part>
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="3">
|
||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||
{{object.findMetadata("dc.description.abstract")}}
|
||||
</div>
|
||||
</ds-truncatable-part>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<a [routerLink]="['/browse/' + object.key + '/' + object.value]" class="lead">
|
||||
{{object.value}}
|
||||
</a>
|
@@ -0,0 +1 @@
|
||||
@import '../../../../styles/variables';
|
@@ -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<MetadataListElementComponent>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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<Metadatum> {}
|
@@ -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 = [
|
||||
|
Reference in New Issue
Block a user