mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
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:
@@ -342,7 +342,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browse": {
|
"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": {
|
"admin": {
|
||||||
"registries": {
|
"registries": {
|
||||||
@@ -439,6 +453,7 @@
|
|||||||
"browse_global_by_issue_date": "By Issue Date",
|
"browse_global_by_issue_date": "By Issue Date",
|
||||||
"browse_global_by_author": "By Author",
|
"browse_global_by_author": "By Author",
|
||||||
"browse_global_by_title": "By Title",
|
"browse_global_by_title": "By Title",
|
||||||
|
"browse_global_by_subject": "By Subject",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"browse_community": "This Community",
|
"browse_community": "This Community",
|
||||||
"browse_community_by_issue_date": "By Issue Date",
|
"browse_community_by_issue_date": "By Issue Date",
|
||||||
|
@@ -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)? '"' + value + '"': ''} }}"
|
|
||||||
[objects$]="(items$ !== undefined)? items$ : authors$"
|
|
||||||
[currentUrl]="currentUrl"
|
|
||||||
[paginationConfig]="paginationConfig"
|
|
||||||
[sortConfig]="sortConfig">
|
|
||||||
</ds-browse-by>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -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)? '"' + value + '"': ''} }}"
|
||||||
|
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||||
|
[paginationConfig]="paginationConfig"
|
||||||
|
[sortConfig]="sortConfig">
|
||||||
|
</ds-browse-by>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -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)));
|
||||||
|
}
|
@@ -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
|
||||||
|
);
|
||||||
|
}
|
@@ -1,9 +1,8 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="browse-by-title w-100 row">
|
<div class="browse-by-title w-100 row">
|
||||||
<ds-browse-by class="col-xs-12 w-100"
|
<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$"
|
[objects$]="items$"
|
||||||
[currentUrl]="currentUrl"
|
|
||||||
[paginationConfig]="paginationConfig"
|
[paginationConfig]="paginationConfig"
|
||||||
[sortConfig]="sortConfig">
|
[sortConfig]="sortConfig">
|
||||||
</ds-browse-by>
|
</ds-browse-by>
|
||||||
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,16 +1,20 @@
|
|||||||
|
import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
||||||
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { Item } from '../../core/shared/item.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 { hasValue } from '../../shared/empty.util';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
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({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
@@ -22,28 +26,44 @@ import { Collection } from '../../core/shared/collection.model';
|
|||||||
*/
|
*/
|
||||||
export class BrowseByTitlePageComponent implements OnInit {
|
export class BrowseByTitlePageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of items to display
|
||||||
|
*/
|
||||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
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(), {
|
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'browse-by-title-pagination',
|
id: 'browse-by-title-pagination',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20
|
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 {
|
ngOnInit(): void {
|
||||||
this.currentUrl = this.route.snapshot.pathFromRoot
|
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||||
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
|
|
||||||
.join('/');
|
|
||||||
this.updatePage({
|
|
||||||
pagination: this.paginationConfig,
|
|
||||||
sort: this.sortConfig
|
|
||||||
});
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.route.params,
|
this.route.params,
|
||||||
@@ -52,22 +72,8 @@ export class BrowseByTitlePageComponent implements OnInit {
|
|||||||
return Object.assign({}, params, queryParams);
|
return Object.assign({}, params, queryParams);
|
||||||
})
|
})
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
this.updatePage(browseParamsToOptions(params, this.paginationConfig, this.sortConfig));
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
this.updateParent(params.scope)
|
||||||
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
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,14 +83,27 @@ export class BrowseByTitlePageComponent implements OnInit {
|
|||||||
* { pagination: PaginationComponentOptions,
|
* { pagination: PaginationComponentOptions,
|
||||||
* sort: SortOptions }
|
* sort: SortOptions }
|
||||||
*/
|
*/
|
||||||
updatePage(searchOptions) {
|
updatePage(searchOptions: BrowseEntrySearchOptions) {
|
||||||
this.items$ = this.itemDataService.findAll({
|
this.items$ = this.itemDataService.findAll({
|
||||||
currentPage: searchOptions.pagination.currentPage,
|
currentPage: searchOptions.pagination.currentPage,
|
||||||
elementsPerPage: searchOptions.pagination.pageSize,
|
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 {
|
ngOnDestroy(): void {
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
|
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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: 'title', component: BrowseByTitlePageComponent },
|
{ path: 'title', component: BrowseByTitlePageComponent },
|
||||||
{ path: 'author', component: BrowseByAuthorPageComponent }
|
{ path: ':metadata', component: BrowseByMetadataPageComponent }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -4,8 +4,8 @@ import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-ti
|
|||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { BrowseByRoutingModule } from './browse-by-routing.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 { BrowseService } from '../core/browse/browse.service';
|
||||||
|
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,7 +15,7 @@ import { BrowseService } from '../core/browse/browse.service';
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
BrowseByAuthorPageComponent
|
BrowseByMetadataPageComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
@@ -7,6 +7,8 @@
|
|||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collection.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
|
<!-- Browse-By Links -->
|
||||||
|
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
||||||
<!-- Collection logo -->
|
<!-- Collection logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||||
<!-- Community name -->
|
<!-- Community name -->
|
||||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
<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 -->
|
<!-- Community logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
|
17
src/app/core/browse/browse-entry-search-options.model.ts
Normal file
17
src/app/core/browse/browse-entry-search-options.model.ts
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,7 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from
|
|||||||
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 { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
@@ -151,14 +152,14 @@ describe('BrowseService', () => {
|
|||||||
it('should configure a new BrowseEntriesRequest', () => {
|
it('should configure a new BrowseEntriesRequest', () => {
|
||||||
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
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();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
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();
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -170,14 +171,14 @@ describe('BrowseService', () => {
|
|||||||
it('should configure a new BrowseItemsRequest', () => {
|
it('should configure a new BrowseItemsRequest', () => {
|
||||||
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
|
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();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
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();
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ describe('BrowseService', () => {
|
|||||||
const definitionID = 'invalidID';
|
const definitionID = 'invalidID';
|
||||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`));
|
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 definitionID = 'invalidID';
|
||||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -33,6 +33,7 @@ import {
|
|||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
@@ -80,18 +81,18 @@ export class BrowseService {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseEntriesFor(definitionID: string, options: {
|
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
pagination?: PaginationComponentOptions;
|
|
||||||
sort?: SortOptions;
|
|
||||||
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(definitionID),
|
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.entries),
|
map((_links: any) => _links.entries),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
// TODO nearly identical to PaginatedSearchOptions => refactor
|
// TODO nearly identical to PaginatedSearchOptions => refactor
|
||||||
const args = [];
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`scope=${options.scope}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
}
|
}
|
||||||
@@ -133,17 +134,17 @@ export class BrowseService {
|
|||||||
* sort: SortOptions }
|
* sort: SortOptions }
|
||||||
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
||||||
*/
|
*/
|
||||||
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
|
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
pagination?: PaginationComponentOptions;
|
|
||||||
sort?: SortOptions;
|
|
||||||
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
|
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(definitionID),
|
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.items),
|
map((_links: any) => _links.items),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const args = [];
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`scope=${options.scope}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
}
|
}
|
||||||
|
@@ -106,21 +106,6 @@ describe('BrowseEntriesResponseParsingService', () => {
|
|||||||
} as DSpaceRESTV2Response;
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
const invalidResponseNotAList = {
|
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,
|
statusCode: 200,
|
||||||
statusText: 'OK'
|
statusText: 'OK'
|
||||||
} as DSpaceRESTV2Response;
|
} as DSpaceRESTV2Response;
|
||||||
|
@@ -30,11 +30,13 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
if (isNotEmpty(data.payload)) {
|
||||||
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
let browseEntries = [];
|
||||||
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
||||||
return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
}
|
||||||
|
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@@ -28,7 +28,13 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
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;
|
let objectList = processRequestDTO;
|
||||||
|
|
||||||
if (hasNoValue(processRequestDTO)) {
|
if (hasNoValue(processRequestDTO)) {
|
||||||
|
@@ -51,28 +51,18 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
} as TextMenuItemModel,
|
} as TextMenuItemModel,
|
||||||
index: 0
|
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',
|
id: 'browse_global_global_by_title',
|
||||||
parentID: 'browse_global',
|
parentID: 'browse_global',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -94,6 +84,17 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/browse/author'
|
link: '/browse/author'
|
||||||
} as LinkMenuItemModel,
|
} 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 */
|
/* Statistics */
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngVar="(objects$ | async) as objects">
|
<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>
|
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="paginationConfig"
|
[config]="paginationConfig"
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
[objects]="objects">
|
[objects]="objects">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
</div>
|
</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>
|
<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>
|
</ng-container>
|
||||||
|
@@ -5,7 +5,6 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
|
|||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { fadeIn, fadeInOut } from '../animations/fade';
|
import { fadeIn, fadeInOut } from '../animations/fade';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -21,10 +20,23 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
|
|||||||
* Component to display a browse-by page for any ListableObject
|
* Component to display a browse-by page for any ListableObject
|
||||||
*/
|
*/
|
||||||
export class BrowseByComponent {
|
export class BrowseByComponent {
|
||||||
|
/**
|
||||||
|
* The i18n message to display as title
|
||||||
|
*/
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of objects to display
|
||||||
|
*/
|
||||||
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination configuration used for the list
|
||||||
|
*/
|
||||||
@Input() paginationConfig: PaginationComponentOptions;
|
@Input() paginationConfig: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting configuration used for the list
|
||||||
|
*/
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
@Input() currentUrl: string;
|
|
||||||
query: string;
|
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
@@ -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;
|
||||||
|
}
|
@@ -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>
|
<h2 *ngIf="title">{{ title | translate }}</h2>
|
||||||
<div *ngIf="hasInnerHtml" [innerHtml]="content"></div>
|
<div *ngIf="hasInnerHtml" [innerHtml]="content"></div>
|
||||||
<div *ngIf="!hasInnerHtml">{{content}}</div>
|
<div *ngIf="!hasInnerHtml">{{content}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="d-flex flex-row">
|
<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}}
|
{{object.value}}
|
||||||
</a>
|
</a>
|
||||||
<span class="pr-2"> </span>
|
<span class="pr-2"> </span>
|
||||||
|
@@ -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 { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||||
|
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -143,6 +144,7 @@ const COMPONENTS = [
|
|||||||
CreateComColPageComponent,
|
CreateComColPageComponent,
|
||||||
EditComColPageComponent,
|
EditComColPageComponent,
|
||||||
DeleteComColPageComponent,
|
DeleteComColPageComponent,
|
||||||
|
ComcolPageBrowseByComponent,
|
||||||
DsDynamicFormComponent,
|
DsDynamicFormComponent,
|
||||||
DsDynamicFormControlContainerComponent,
|
DsDynamicFormControlContainerComponent,
|
||||||
DsDynamicListComponent,
|
DsDynamicListComponent,
|
||||||
|
Reference in New Issue
Block a user