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": {
|
"admin": {
|
||||||
"registries": {
|
"registries": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@@ -193,7 +196,8 @@
|
|||||||
"recent-submissions": "Loading recent submissions...",
|
"recent-submissions": "Loading recent submissions...",
|
||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading...",
|
"objects": "Loading...",
|
||||||
"search-results": "Loading search results..."
|
"search-results": "Loading search results...",
|
||||||
|
"browse-by": "Loading items..."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"default": "Error",
|
"default": "Error",
|
||||||
@@ -205,6 +209,7 @@
|
|||||||
"item": "Error fetching item",
|
"item": "Error fetching item",
|
||||||
"objects": "Error fetching objects",
|
"objects": "Error fetching objects",
|
||||||
"search-results": "Error fetching search results",
|
"search-results": "Error fetching search results",
|
||||||
|
"browse-by": "Error fetching items",
|
||||||
"validation": {
|
"validation": {
|
||||||
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
||||||
"license": {
|
"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: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ 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: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ 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 { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
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 { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { BrowseEntry } from '../shared/browse-entry.model';
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses,
|
filterSuccessfulResponses, getBrowseDefinitionLinks,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getRequestFromSelflink,
|
getRequestFromSelflink,
|
||||||
getResponseFromSelflink
|
getResponseFromSelflink
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
@@ -71,6 +79,8 @@ export class BrowseService {
|
|||||||
map((entry: ResponseCacheEntry) => entry.response),
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||||
ensureArrayHasValue(),
|
ensureArrayHasValue(),
|
||||||
|
map((definitions: BrowseDefinition[]) => definitions
|
||||||
|
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,17 +92,7 @@ export class BrowseService {
|
|||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
getRemoteDataPayload(),
|
getBrowseDefinitionLinks(definitionID),
|
||||||
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}'`);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.entries),
|
map((_links: any) => _links.entries),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
@@ -124,6 +124,57 @@ export class BrowseService {
|
|||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
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()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -62,6 +62,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me
|
|||||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||||
|
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -114,6 +115,7 @@ const PROVIDERS = [
|
|||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
BrowseEntriesResponseParsingService,
|
BrowseEntriesResponseParsingService,
|
||||||
|
BrowseItemsResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
RouteService,
|
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 { CoreState } from '../core.reducers';
|
||||||
import { ComColDataService } from './comcol-data.service';
|
import { ComColDataService } from './comcol-data.service';
|
||||||
import { CommunityDataService } from './community-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 { RequestService } from './request.service';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
@@ -53,6 +53,10 @@ describe('ComColDataService', () => {
|
|||||||
const EnvConfig = {} as GlobalConfig;
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
|
||||||
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||||
|
const options = Object.assign(new FindAllOptions(), {
|
||||||
|
scopeID: scopeID
|
||||||
|
});
|
||||||
|
|
||||||
const communitiesEndpoint = 'https://rest.api/core/communities';
|
const communitiesEndpoint = 'https://rest.api/core/communities';
|
||||||
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
||||||
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
||||||
@@ -99,7 +103,7 @@ describe('ComColDataService', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('getScopedEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
});
|
});
|
||||||
@@ -113,7 +117,7 @@ describe('ComColDataService', () => {
|
|||||||
|
|
||||||
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
|
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
@@ -129,13 +133,13 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch the scope Community from the cache', () => {
|
it('should fetch the scope Community from the cache', () => {
|
||||||
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
|
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the endpoint to fetch resources within the given scope', () => {
|
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 });
|
const expected = cold('--e-', { e: scopedEndpoint });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
@@ -152,7 +156,7 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error', () => {
|
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`));
|
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
|
@@ -8,7 +8,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
|||||||
import { CommunityDataService } from './community-data.service';
|
import { CommunityDataService } from './community-data.service';
|
||||||
|
|
||||||
import { DataService } from './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 { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@@ -27,16 +27,16 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
|
|||||||
* @return { Observable<string> }
|
* @return { Observable<string> }
|
||||||
* an Observable<string> containing the scoped URL
|
* an Observable<string> containing the scoped URL
|
||||||
*/
|
*/
|
||||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
|
||||||
if (isEmpty(scopeID)) {
|
if (isEmpty(options.scopeID)) {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
} else {
|
} else {
|
||||||
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
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))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.take(1)
|
.take(1)
|
||||||
.do((href: string) => {
|
.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);
|
this.requestService.configure(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
|
|||||||
|
|
||||||
return Observable.merge(
|
return Observable.merge(
|
||||||
errorResponse.flatMap((response: ErrorResponse) =>
|
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
|
successResponse
|
||||||
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID))
|
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(options.scopeID))
|
||||||
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
|
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
|
||||||
.filter((href) => isNotEmpty(href))
|
.filter((href) => isNotEmpty(href))
|
||||||
).distinctUntilChanged();
|
).distinctUntilChanged();
|
||||||
|
@@ -20,17 +20,13 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
|||||||
protected abstract linkPath: string;
|
protected abstract linkPath: string;
|
||||||
protected abstract halService: HALEndpointService;
|
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>;
|
let result: Observable<string>;
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
if (hasValue(options.scopeID)) {
|
result = this.getBrowseEndpoint(options).distinctUntilChanged();
|
||||||
result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged();
|
|
||||||
} else {
|
|
||||||
result = Observable.of(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
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 */
|
/* 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>>> {
|
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
|
const hrefObs = this.getFindAllHref(options);
|
||||||
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
.filter((href: string) => hasValue(href))
|
.filter((href: string) => hasValue(href))
|
||||||
|
@@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { FindAllOptions } from './request.models';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -20,9 +21,19 @@ describe('ItemDataService', () => {
|
|||||||
const halEndpointService = {} as HALEndpointService;
|
const halEndpointService = {} as HALEndpointService;
|
||||||
|
|
||||||
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
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 browsesEndpoint = 'https://rest.api/discover/browses';
|
||||||
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
|
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 serviceEndpoint = `https://rest.api/core/items`;
|
||||||
const browseError = new Error('getBrowseURL failed');
|
const browseError = new Error('getBrowseURL failed');
|
||||||
|
|
||||||
@@ -46,16 +57,16 @@ describe('ItemDataService', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('getScopedEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
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);
|
bs = initMockBrowseService(true);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
const result = service.getScopedEndpoint(scopeID);
|
const result = service.getBrowseEndpoint(options);
|
||||||
const expected = cold('--b-', { b: scopedEndpoint });
|
const expected = cold('--b-', { b: scopedEndpoint });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
@@ -67,7 +78,7 @@ describe('ItemDataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
const result = service.getScopedEndpoint(scopeID);
|
const result = service.getBrowseEndpoint(options);
|
||||||
const expected = cold('--#-', undefined, browseError);
|
const expected = cold('--#-', undefined, browseError);
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
|
@@ -15,6 +15,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { FindAllOptions } from './request.models';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
@@ -30,15 +31,15 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
|
||||||
if (isEmpty(scopeID)) {
|
let field = 'dc.date.issued';
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
if (options.sort && options.sort.field) {
|
||||||
} else {
|
field = options.sort.field;
|
||||||
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath)
|
|
||||||
.filter((href: string) => isNotEmpty(href))
|
|
||||||
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
}
|
||||||
|
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 { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||||
|
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ export class FindAllOptions {
|
|||||||
elementsPerPage?: number;
|
elementsPerPage?: number;
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
|
startsWith?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FindAllRequest extends GetRequest {
|
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 {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { autoserialize, autoserializeAs } from 'cerialize';
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
export class BrowseEntry {
|
export class BrowseEntry implements ListableObject {
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
type: string;
|
type: string;
|
||||||
|
@@ -5,6 +5,7 @@ import { RemoteData } from '../data/remote-data';
|
|||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { autoserialize } from 'cerialize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a DSpaceObject.
|
* An abstract model class for a DSpaceObject.
|
||||||
@@ -16,11 +17,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The human-readable identifier of this DSpaceObject
|
* The human-readable identifier of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The universally unique identifier of this DSpaceObject
|
* The universally unique identifier of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,11 +34,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The name for this DSpaceObject
|
* The name for this DSpaceObject
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* An array containing all metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
|
@autoserialize
|
||||||
metadata: Metadatum[];
|
metadata: Metadatum[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
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 { DSOSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
@@ -8,6 +8,7 @@ import { RemoteData } from '../data/remote-data';
|
|||||||
import { RestRequest } from '../data/request.models';
|
import { RestRequest } from '../data/request.models';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { RequestService } from '../data/request.service';
|
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
|
* 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 = () =>
|
export const getRemoteDataPayload = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||||
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
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"
|
<ds-object-grid [config]="config"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="objects"
|
[objects]="objects"
|
||||||
[hideGear]="true"
|
[hideGear]="hideGear"
|
||||||
*ngIf="getViewMode()===viewModeEnum.Grid">
|
*ngIf="getViewMode()===viewModeEnum.Grid">
|
||||||
</ds-object-grid>
|
</ds-object-grid>
|
||||||
|
|
||||||
|
@@ -1,22 +1,28 @@
|
|||||||
|
<ds-truncatable [id]="object.id">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<a [routerLink]="['/items/', object.id]" class="card-img-top">
|
<a [routerLink]="['/items/', object.id]" class="card-img-top">
|
||||||
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
||||||
</ds-grid-thumbnail>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
||||||
|
|
||||||
|
<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">
|
<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 *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||||
<span *ngIf="!last">; </span>
|
<span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||||
</p>
|
</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">
|
<div class="text-center pt-2">
|
||||||
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||||
</div>
|
</div>
|
||||||
</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,7 +1,9 @@
|
|||||||
|
<ds-truncatable [id]="object.id">
|
||||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||||
{{object.findMetadata("dc.title")}}
|
{{object.findMetadata("dc.title")}}
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
|
<ds-truncatable-part [id]="object.id" [minLines]="1">
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||||
class="item-list-authors">
|
class="item-list-authors">
|
||||||
@@ -12,7 +14,11 @@
|
|||||||
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </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>)
|
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||||
</span>
|
</span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<ds-truncatable-part [id]="object.id" [minLines]="3">
|
||||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||||
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
|
{{object.findMetadata("dc.description.abstract")}}
|
||||||
</div>
|
</div>
|
||||||
|
</ds-truncatable-part>
|
||||||
</div>
|
</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 { 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 { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
|
||||||
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
|
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 = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -139,11 +143,13 @@ const COMPONENTS = [
|
|||||||
ViewModeSwitchComponent,
|
ViewModeSwitchComponent,
|
||||||
TruncatableComponent,
|
TruncatableComponent,
|
||||||
TruncatablePartComponent,
|
TruncatablePartComponent,
|
||||||
|
BrowseByComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put shared entry components (components that are created dynamically) here
|
// put shared entry components (components that are created dynamically) here
|
||||||
ItemListElementComponent,
|
ItemListElementComponent,
|
||||||
|
MetadataListElementComponent,
|
||||||
CollectionListElementComponent,
|
CollectionListElementComponent,
|
||||||
CommunityListElementComponent,
|
CommunityListElementComponent,
|
||||||
SearchResultListElementComponent,
|
SearchResultListElementComponent,
|
||||||
@@ -151,6 +157,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
CollectionGridElementComponent,
|
CollectionGridElementComponent,
|
||||||
CommunityGridElementComponent,
|
CommunityGridElementComponent,
|
||||||
SearchResultGridElementComponent,
|
SearchResultGridElementComponent,
|
||||||
|
BrowseEntryListElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
Reference in New Issue
Block a user