Merge remote-tracking branch 'remotes/origin/master' into submission

# Conflicts:
#	src/app/core/data/browse-entries-response-parsing.service.spec.ts
#	src/app/core/data/browse-entries-response-parsing.service.ts
This commit is contained in:
Giuseppe Digilio
2019-02-21 18:49:36 +01:00
28 changed files with 634 additions and 224 deletions

View File

@@ -342,7 +342,21 @@
}
},
"browse": {
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
"title": "Browsing {{ collection }} by {{ field }} {{ value }}",
"metadata": {
"title": "Title",
"author": "Author",
"subject": "Subject"
},
"comcol": {
"head": "Browse",
"by": {
"title": "By Title",
"author": "By Author",
"subject": "By Subject"
}
},
"empty": "No items to show."
},
"admin": {
"registries": {
@@ -439,6 +453,7 @@
"browse_global_by_issue_date": "By Issue Date",
"browse_global_by_author": "By Author",
"browse_global_by_title": "By Title",
"browse_global_by_subject": "By Subject",
"statistics": "Statistics",
"browse_community": "This Community",
"browse_community_by_issue_date": "By Issue Date",

View File

@@ -1,11 +0,0 @@
<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)? '&quot;' + value + '&quot;': ''} }}"
[objects$]="(items$ !== undefined)? items$ : authors$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -1,107 +0,0 @@
import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-author-page',
styleUrls: ['./browse-by-author-page.component.scss'],
templateUrl: './browse-by-author-page.component.html'
})
/**
* Component for browsing (items) by author (dc.contributor.author)
*/
export class BrowseByAuthorPageComponent implements OnInit {
authors$: Observable<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(
observableCombineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
this.value = +params.value || params.value || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
const searchOptions = {
pagination: pagination,
sort: sort
};
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
this.items$ = undefined;
}
/**
* Updates the current page with searchOptions and display items linked to author
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @param author The author's name for displaying items
*/
updatePageWithItems(searchOptions, author: string) {
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,10 @@
<div class="container">
<div class="browse-by-metadata w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + metadata | translate, value: (value)? '&quot;' + value + '&quot;': ''} }}"
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,162 @@
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowseService } from '../../core/browse/browse.service';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { PageInfo } from '../../core/shared/page-info.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { SortDirection } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { Community } from '../../core/shared/community.model';
describe('BrowseByMetadataPageComponent', () => {
let comp: BrowseByMetadataPageComponent;
let fixture: ComponentFixture<BrowseByMetadataPageComponent>;
let browseService: BrowseService;
let route: ActivatedRoute;
const mockCommunity = Object.assign(new Community(), {
id: 'test-uuid',
metadata: [
{
key: 'dc.title',
value: 'test community'
}
]
});
const mockEntries = [
{
type: 'author',
authority: null,
value: 'John Doe',
language: 'en',
count: 1
},
{
type: 'author',
authority: null,
value: 'James Doe',
language: 'en',
count: 3
},
{
type: 'subject',
authority: null,
value: 'Fake subject',
language: 'en',
count: 2
}
];
const mockItems = [
Object.assign(new Item(), {
id: 'fakeId'
})
];
const mockBrowseService = {
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData(mockEntries.filter((entry) => entry.type === options.metadataDefinition)),
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData(mockItems)
};
const mockDsoService = {
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
};
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: observableOf({})
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BrowseService, useValue: mockBrowseService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByMetadataPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
browseService = (comp as any).browseService;
route = (comp as any).route;
route.params = observableOf({});
comp.ngOnInit();
fixture.detectChanges();
});
it('should fetch the correct entries depending on the metadata definition', () => {
comp.browseEntries$.subscribe((result) => {
expect(result.payload.page).toEqual(mockEntries.filter((entry) => entry.type === 'author'));
});
});
it('should not fetch any items when no value is provided', () => {
expect(comp.items$).toBeUndefined();
});
describe('when a value is provided', () => {
beforeEach(() => {
const paramsWithValue = {
metadata: 'author',
value: 'John Doe'
};
route.params = observableOf(paramsWithValue);
comp.ngOnInit();
});
it('should fetch items', () => {
comp.items$.subscribe((result) => {
expect(result.payload.page).toEqual(mockItems);
});
})
});
describe('when calling browseParamsToOptions', () => {
let result: BrowseEntrySearchOptions;
beforeEach(() => {
const paramsWithPaginationAndScope = {
page: 5,
pageSize: 10,
sortDirection: SortDirection.ASC,
sortField: 'fake-field',
scope: 'fake-scope'
};
result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author');
});
it('should return BrowseEntrySearchOptions with the correct properties', () => {
expect(result.metadataDefinition).toEqual('author');
expect(result.pagination.currentPage).toEqual(5);
expect(result.pagination.pageSize).toEqual(10);
expect(result.sort.direction).toEqual(SortDirection.ASC);
expect(result.sort.field).toEqual('fake-field');
expect(result.scope).toEqual('fake-scope');
})
});
});
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
return observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), objects)));
}

View File

@@ -0,0 +1,182 @@
import {combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { Community } from '../../core/shared/community.model';
import { Collection } from '../../core/shared/collection.model';
import { getSucceededRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
@Component({
selector: 'ds-browse-by-metadata-page',
styleUrls: ['./browse-by-metadata-page.component.scss'],
templateUrl: './browse-by-metadata-page.component.html'
})
/**
* Component for browsing (items) by metadata definition
* A metadata definition is a short term used to describe one or multiple metadata fields.
* An example would be 'author' for 'dc.contributor.*'
*/
export class BrowseByMetadataPageComponent implements OnInit {
/**
* The list of browse-entries to display
*/
browseEntries$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
/**
* The list of items to display when a value is present
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* The current Community or Collection we're browsing metadata/items in
*/
parent$: Observable<RemoteData<DSpaceObject>>;
/**
* The pagination config used to display the values
*/
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-metadata-pagination',
currentPage: 1,
pageSize: 20
});
/**
* The sorting config used to sort the values (defaults to Ascending)
*/
sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC);
/**
* List of subscriptions
*/
subs: Subscription[] = [];
/**
* The default metadata definition to resort to when none is provided
*/
defaultMetadata = 'author';
/**
* The current metadata definition
*/
metadata = this.defaultMetadata;
/**
* The value we're browing items for
* - When the value is not empty, we're browsing items
* - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
*/
value = '';
public constructor(private route: ActivatedRoute,
private browseService: BrowseService,
private dsoService: DSpaceObjectDataService) {
}
ngOnInit(): void {
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
this.subs.push(
observableCombineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
this.metadata = params.metadata || this.defaultMetadata;
this.value = +params.value || params.value || '';
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
this.updateParent(params.scope);
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { metadata: string
* pagination: PaginationComponentOptions,
* sort: SortOptions,
* scope: string }
*/
updatePage(searchOptions: BrowseEntrySearchOptions) {
this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions);
this.items$ = undefined;
}
/**
* Updates the current page with searchOptions and display items linked to the given value
* @param searchOptions Options to narrow down your search:
* { metadata: string
* pagination: PaginationComponentOptions,
* sort: SortOptions,
* scope: string }
* @param value The value of the browse-entry to display items for
*/
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
}
/**
* Update the parent Community or Collection using their scope
* @param scope The UUID of the Community or Collection to fetch
*/
updateParent(scope: string) {
if (hasValue(scope)) {
this.parent$ = this.dsoService.findById(scope).pipe(
getSucceededRemoteData()
);
}
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}
/**
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
* @param params URL and query parameters
* @param paginationConfig Pagination configuration
* @param sortConfig Sorting configuration
* @param metadata Optional metadata definition to fetch browse entries/items for
*/
export function browseParamsToOptions(params: any,
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions,
metadata?: string): BrowseEntrySearchOptions {
return new BrowseEntrySearchOptions(
metadata,
Object.assign({},
paginationConfig,
{
currentPage: +params.page || paginationConfig.currentPage,
pageSize: +params.pageSize || paginationConfig.pageSize
}
),
Object.assign({},
sortConfig,
{
direction: params.sortDirection || sortConfig.direction,
field: params.sortField || sortConfig.field
}
),
params.scope
);
}

View File

@@ -1,9 +1,8 @@
<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: ''} }}"
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.title' | translate, value: ''} }}"
[objects$]="items$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>

View File

@@ -0,0 +1,85 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { Item } from '../../core/shared/item.model';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
import { ItemDataService } from '../../core/data/item-data.service';
import { Community } from '../../core/shared/community.model';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
describe('BrowseByTitlePageComponent', () => {
let comp: BrowseByTitlePageComponent;
let fixture: ComponentFixture<BrowseByTitlePageComponent>;
let itemDataService: ItemDataService;
let route: ActivatedRoute;
const mockCommunity = Object.assign(new Community(), {
id: 'test-uuid',
metadata: [
{
key: 'dc.title',
value: 'test community'
}
]
});
const mockItems = [
Object.assign(new Item(), {
id: 'fakeId',
metadata: [
{
key: 'dc.title',
value: 'Fake Title'
}
]
})
];
const mockItemDataService = {
findAll: () => toRemoteData(mockItems)
};
const mockDsoService = {
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
};
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: observableOf({})
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: ItemDataService, useValue: mockItemDataService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByTitlePageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
itemDataService = (comp as any).itemDataService;
route = (comp as any).route;
});
it('should initialize the list of items', () => {
comp.items$.subscribe((result) => {
expect(result.payload.page).toEqual(mockItems);
});
});
});

View File

@@ -1,16 +1,20 @@
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
import { browseParamsToOptions } from '../+browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { Community } from '../../core/shared/community.model';
import { getSucceededRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
@Component({
selector: 'ds-browse-by-title-page',
@@ -22,28 +26,44 @@ import { Collection } from '../../core/shared/collection.model';
*/
export class BrowseByTitlePageComponent implements OnInit {
/**
* The list of items to display
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* The current Community or Collection we're browsing metadata/items in
*/
parent$: Observable<RemoteData<DSpaceObject>>;
/**
* The pagination configuration to use for displaying the list of items
*/
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
/**
* The sorting configuration to use for displaying the list of items
* Sorted by title (Ascending by default)
*/
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
/**
* List of subscriptions
*/
subs: Subscription[] = [];
public constructor(private itemDataService: ItemDataService,
private route: ActivatedRoute,
private dsoService: DSpaceObjectDataService) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
this.subs.push(
observableCombineLatest(
this.route.params,
@@ -52,22 +72,8 @@ export class BrowseByTitlePageComponent implements OnInit {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
this.updatePage({
pagination: pagination,
sort: sort
});
this.updatePage(browseParamsToOptions(params, this.paginationConfig, this.sortConfig));
this.updateParent(params.scope)
}));
}
@@ -77,14 +83,27 @@ export class BrowseByTitlePageComponent implements OnInit {
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
updatePage(searchOptions: BrowseEntrySearchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
sort: searchOptions.sort
sort: searchOptions.sort,
scopeID: searchOptions.scope
});
}
/**
* Update the parent Community or Collection using their scope
* @param scope The UUID of the Community or Collection to fetch
*/
updateParent(scope: string) {
if (hasValue(scope)) {
this.parent$ = this.dsoService.findById(scope).pipe(
getSucceededRemoteData()
);
}
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}

View File

@@ -1,13 +1,13 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'title', component: BrowseByTitlePageComponent },
{ path: 'author', component: BrowseByAuthorPageComponent }
{ path: ':metadata', component: BrowseByMetadataPageComponent }
])
]
})

View File

@@ -4,8 +4,8 @@ import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-ti
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
@NgModule({
imports: [
@@ -15,7 +15,7 @@ import { BrowseService } from '../core/browse/browse.service';
],
declarations: [
BrowseByTitlePageComponent,
BrowseByAuthorPageComponent
BrowseByMetadataPageComponent
],
providers: [
ItemDataService,

View File

@@ -7,6 +7,8 @@
<ds-comcol-page-header
[name]="collection.name">
</ds-comcol-page-header>
<!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"

View File

@@ -3,6 +3,8 @@
<div *ngIf="communityRD?.payload; let communityPayload">
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="communityPayload.id"></ds-comcol-page-browse-by>
<!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"

View File

@@ -0,0 +1,17 @@
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortOptions } from '../cache/models/sort-options.model';
/**
* A class that defines the search options to be used for fetching browse entries or items
* - metadataDefinition: The metadata definition to fetch entries or items for
* - pagination: Optional pagination options to use
* - sort: Optional sorting options to use
* - scope: An optional scope to limit the results within a specific collection or community
*/
export class BrowseEntrySearchOptions {
constructor(public metadataDefinition: string,
public pagination?: PaginationComponentOptions,
public sort?: SortOptions,
public scope?: string) {
}
}

View File

@@ -8,6 +8,7 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { RequestEntry } from '../data/request.reducer';
import { of as observableOf } from 'rxjs';
@@ -151,14 +152,14 @@ describe('BrowseService', () => {
it('should configure a new BrowseEntriesRequest', () => {
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe());
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseEntriesFor(browseDefinitions[1].id);
service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id));
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
@@ -170,14 +171,14 @@ describe('BrowseService', () => {
it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id));
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
@@ -191,7 +192,7 @@ describe('BrowseService', () => {
const definitionID = 'invalidID';
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`));
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
});
});
@@ -201,7 +202,7 @@ describe('BrowseService', () => {
const definitionID = 'invalidID';
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
expect(service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
});
});
});

View File

@@ -33,6 +33,7 @@ import {
import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
@Injectable()
export class BrowseService {
@@ -80,18 +81,18 @@ export class BrowseService {
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
getBrowseEntriesFor(definitionID: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(),
map((_links: any) => _links.entries),
hasValueOperator(),
map((href: string) => {
// TODO nearly identical to PaginatedSearchOptions => refactor
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`scope=${options.scope}`);
}
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
@@ -133,17 +134,17 @@ export class BrowseService {
* sort: SortOptions }
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
getBrowseDefinitionLinks(options.metadataDefinition),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`scope=${options.scope}`);
}
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}

View File

@@ -106,21 +106,6 @@ describe('BrowseEntriesResponseParsingService', () => {
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
payload: {
authority: null,
value: 'Arulmozhiyal, Ramaswamy',
valueLang: null,
count: 1,
type: 'browseEntry',
_links: {
self: {
href: 'https://rest.api/discover/browses/author/entries'
},
items: {
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
}
},
},
statusCode: 200,
statusText: 'OK'
} as DSpaceRESTV2Response;

View File

@@ -30,11 +30,13 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
if (isNotEmpty(data.payload)) {
let browseEntries = [];
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
}
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(

View File

@@ -28,7 +28,13 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const processRequestDTO = this.process<NormalizedObject, ResourceType>(data.payload, request.uuid);
let processRequestDTO;
// Prevent empty pages returning an error, initialize empty array instead.
if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) {
processRequestDTO = { page: [] };
} else {
processRequestDTO = this.process<NormalizedObject, ResourceType>(data.payload, request.uuid);
}
let objectList = processRequestDTO;
if (hasNoValue(processRequestDTO)) {

View File

@@ -51,28 +51,18 @@ export class NavbarComponent extends MenuComponent implements OnInit {
} as TextMenuItemModel,
index: 0
},
// {
// id: 'browse_global_communities_and_collections',
// parentID: 'browse_global',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.browse_global_communities_and_collections',
// link: '#'
// } as LinkMenuItemModel,
// },
{
id: 'browse_global_communities_and_collections',
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.browse_global_communities_and_collections',
link: '#'
} as LinkMenuItemModel,
},
{
id: 'browse_global_global_by_issue_date',
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.browse_global_by_issue_date',
link: '#'
} as LinkMenuItemModel,
}, {
id: 'browse_global_global_by_title',
parentID: 'browse_global',
active: false,
@@ -94,6 +84,17 @@ export class NavbarComponent extends MenuComponent implements OnInit {
link: '/browse/author'
} as LinkMenuItemModel,
},
{
id: 'browse_global_by_subject',
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.browse_global_by_subject',
link: '/browse/subject'
} as LinkMenuItemModel,
},
/* Statistics */
{

View File

@@ -1,5 +1,5 @@
<ng-container *ngVar="(objects$ | async) as objects">
<h2 class="w-100">{{title}}</h2>
<h2 class="w-100">{{title | translate}}</h2>
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
<ds-viewable-collection
[config]="paginationConfig"
@@ -7,6 +7,9 @@
[objects]="objects">
</ds-viewable-collection>
</div>
<ds-loading *ngIf="!objects || objects?.payload?.page.length <= 0" message="{{'loading.browse-by' | translate}}"></ds-loading>
<ds-loading *ngIf="!objects || objects?.isLoading" message="{{'loading.browse-by' | translate}}"></ds-loading>
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
<div *ngIf="!objects?.isLoading && objects?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
{{'browse.empty' | translate}}
</div>
</ng-container>

View File

@@ -5,7 +5,6 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { Observable } from 'rxjs';
import { Item } from '../../core/shared/item.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
@Component({
@@ -21,10 +20,23 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
* Component to display a browse-by page for any ListableObject
*/
export class BrowseByComponent {
/**
* The i18n message to display as title
*/
@Input() title: string;
/**
* The list of objects to display
*/
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
/**
* The pagination configuration used for the list
*/
@Input() paginationConfig: PaginationComponentOptions;
/**
* The sorting configuration used for the list
*/
@Input() sortConfig: SortOptions;
@Input() currentUrl: string;
query: string;
}

View File

@@ -0,0 +1,6 @@
<h3>{{'browse.comcol.head' | translate}}</h3>
<ul>
<li><a [routerLink]="['/browse/title']" [queryParams]="{scope: id}">{{'browse.comcol.by.title' | translate}}</a></li>
<li><a [routerLink]="['/browse/author']" [queryParams]="{scope: id}">{{'browse.comcol.by.author' | translate}}</a></li>
<li><a [routerLink]="['/browse/subject']" [queryParams]="{scope: id}">{{'browse.comcol.by.subject' | translate}}</a></li>
</ul>

View File

@@ -0,0 +1,16 @@
import { Component, Input } from '@angular/core';
/**
* A component to display the "Browse By" section of a Community or Collection page
* It expects the ID of the Community or Collection as input to be passed on as a scope
*/
@Component({
selector: 'ds-comcol-page-browse-by',
templateUrl: './comcol-page-browse-by.component.html',
})
export class ComcolPageBrowseByComponent {
/**
* The ID of the Community or Collection
*/
@Input() id: string;
}

View File

@@ -1,5 +1,5 @@
<div *ngIf="content" class="content-with-optional-title">
<div *ngIf="content" class="content-with-optional-title mb-2">
<h2 *ngIf="title">{{ title | translate }}</h2>
<div *ngIf="hasInnerHtml" [innerHtml]="content"></div>
<div *ngIf="!hasInnerHtml">{{content}}</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<div class="d-flex flex-row">
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
<a [routerLink]="" [queryParams]="{value: object.value}" [queryParamsHandling]="'merge'" class="lead">
{{object.value}}
</a>
<span class="pr-2">&nbsp;</span>

View File

@@ -94,6 +94,7 @@ import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/cre
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -143,6 +144,7 @@ const COMPONENTS = [
CreateComColPageComponent,
EditComColPageComponent,
DeleteComColPageComponent,
ComcolPageBrowseByComponent,
DsDynamicFormComponent,
DsDynamicFormControlContainerComponent,
DsDynamicListComponent,