mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into configurable_entities
This commit is contained in:
@@ -80,6 +80,15 @@ module.exports = {
|
|||||||
label: 'Nederlands',
|
label: 'Nederlands',
|
||||||
active: false,
|
active: false,
|
||||||
}],
|
}],
|
||||||
|
// Browse-By Pages
|
||||||
|
browseBy: {
|
||||||
|
// Amount of years to display using jumps of one year (current year - oneYearLimit)
|
||||||
|
oneYearLimit: 10,
|
||||||
|
// Limit for years to display using jumps of five years (current year - fiveYearLimit)
|
||||||
|
fiveYearLimit: 30,
|
||||||
|
// The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
|
||||||
|
defaultLowerLimit: 1900
|
||||||
|
},
|
||||||
item: {
|
item: {
|
||||||
edit: {
|
edit: {
|
||||||
undoTimeout: 10000 // 10 seconds
|
undoTimeout: 10000 // 10 seconds
|
||||||
|
@@ -492,15 +492,40 @@
|
|||||||
},
|
},
|
||||||
"browse": {
|
"browse": {
|
||||||
"title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
"title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||||
|
"startsWith": {
|
||||||
|
"jump": "Jump to a point in the index:",
|
||||||
|
"choose_year": "(Choose year)",
|
||||||
|
"choose_start": "(Choose start)",
|
||||||
|
"type_date": "Or type in a date (year-month):",
|
||||||
|
"type_text": "Or enter first few letters:",
|
||||||
|
"months": {
|
||||||
|
"none": "(Choose month)",
|
||||||
|
"january": "January",
|
||||||
|
"february": "February",
|
||||||
|
"march": "March",
|
||||||
|
"april": "April",
|
||||||
|
"may": "May",
|
||||||
|
"june": "June",
|
||||||
|
"july": "July",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "October",
|
||||||
|
"november": "November",
|
||||||
|
"december": "December"
|
||||||
|
},
|
||||||
|
"submit": "Go"
|
||||||
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
"subject": "Subject"
|
"subject": "Subject",
|
||||||
|
"dateissued": "Issue Date"
|
||||||
},
|
},
|
||||||
"comcol": {
|
"comcol": {
|
||||||
"head": "Browse",
|
"head": "Browse",
|
||||||
"by": {
|
"by": {
|
||||||
"title": "By Title",
|
"title": "By Title",
|
||||||
|
"dateissued": "By Issue Date",
|
||||||
"author": "By Author",
|
"author": "By Author",
|
||||||
"subject": "By Subject"
|
"subject": "By Subject"
|
||||||
}
|
}
|
||||||
@@ -681,7 +706,8 @@
|
|||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading...",
|
"objects": "Loading...",
|
||||||
"search-results": "Loading search results...",
|
"search-results": "Loading search results...",
|
||||||
"browse-by": "Loading items..."
|
"browse-by": "Loading items...",
|
||||||
|
"browse-by-page": "Loading page..."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"default": "Error",
|
"default": "Error",
|
||||||
|
@@ -0,0 +1,104 @@
|
|||||||
|
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
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 { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
|
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||||
|
|
||||||
|
describe('BrowseByDatePageComponent', () => {
|
||||||
|
let comp: BrowseByDatePageComponent;
|
||||||
|
let fixture: ComponentFixture<BrowseByDatePageComponent>;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
|
||||||
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
id: 'test-uuid',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'test community'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstItem = Object.assign(new Item(), {
|
||||||
|
id: 'first-item-id',
|
||||||
|
metadata: {
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
value: '1950-01-01'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockBrowseService = {
|
||||||
|
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
||||||
|
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
||||||
|
getFirstItemFor: () => observableOf(new RemoteData(false, false, true, undefined, firstItem))
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDsoService = {
|
||||||
|
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||||
|
};
|
||||||
|
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({}),
|
||||||
|
queryParams: observableOf({}),
|
||||||
|
data: observableOf({ metadata: 'dateissued', metadataField: 'dc.date.issued' })
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCdRef = Object.assign({
|
||||||
|
detectChanges: () => fixture.detectChanges()
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [BrowseByDatePageComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
|
{ provide: Router, useValue: new MockRouter() },
|
||||||
|
{ provide: ChangeDetectorRef, useValue: mockCdRef }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BrowseByDatePageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
route = (comp as any).route;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize the list of items', () => {
|
||||||
|
comp.items$.subscribe((result) => {
|
||||||
|
expect(result.payload.page).toEqual([firstItem]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a list of startsWith options with the earliest year at the end (rounded down by 10)', () => {
|
||||||
|
expect(comp.startsWithOptions[comp.startsWithOptions.length - 1]).toEqual(1950);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a list of startsWith options with the current year first', () => {
|
||||||
|
expect(comp.startsWithOptions[0]).toEqual(new Date().getFullYear());
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,116 @@
|
|||||||
|
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
BrowseByMetadataPageComponent,
|
||||||
|
browseParamsToOptions
|
||||||
|
} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-browse-by-date-page',
|
||||||
|
styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
|
||||||
|
templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for browsing items by metadata definition of type 'date'
|
||||||
|
* A metadata definition is a short term used to describe one or multiple metadata fields.
|
||||||
|
* An example would be 'dateissued' for 'dc.date.issued'
|
||||||
|
*/
|
||||||
|
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default metadata-field to use for determining the lower limit of the StartsWith dropdown options
|
||||||
|
*/
|
||||||
|
defaultMetadataField = 'dc.date.issued';
|
||||||
|
|
||||||
|
public constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected browseService: BrowseService,
|
||||||
|
protected dsoService: DSpaceObjectDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected cdRef: ChangeDetectorRef) {
|
||||||
|
super(route, browseService, dsoService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.startsWithType = StartsWithType.date;
|
||||||
|
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest(
|
||||||
|
this.route.params,
|
||||||
|
this.route.queryParams,
|
||||||
|
this.route.data,
|
||||||
|
(params, queryParams, data ) => {
|
||||||
|
return Object.assign({}, params, queryParams, data);
|
||||||
|
})
|
||||||
|
.subscribe((params) => {
|
||||||
|
const metadataField = params.metadataField || this.defaultMetadataField;
|
||||||
|
this.metadata = params.metadata || this.defaultMetadata;
|
||||||
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
|
const searchOptions = browseParamsToOptions(params, Object.assign({}), this.sortConfig, this.metadata);
|
||||||
|
this.updatePageWithItems(searchOptions, this.value);
|
||||||
|
this.updateParent(params.scope);
|
||||||
|
this.updateStartsWithOptions(this.metadata, metadataField, params.scope);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the StartsWith options
|
||||||
|
* In this implementation, it creates a list of years starting from now, going all the way back to the earliest
|
||||||
|
* date found on an item within this scope. The further back in time, the bigger the change in years become to avoid
|
||||||
|
* extremely long lists with a one-year difference.
|
||||||
|
* To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this.
|
||||||
|
* @param definition The metadata definition to fetch the first item for
|
||||||
|
* @param metadataField The metadata field to fetch the earliest date from (expects a date field)
|
||||||
|
* @param scope The scope under which to fetch the earliest item for
|
||||||
|
*/
|
||||||
|
updateStartsWithOptions(definition: string, metadataField: string, scope?: string) {
|
||||||
|
this.subs.push(
|
||||||
|
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
||||||
|
let lowerLimit = this.config.browseBy.defaultLowerLimit;
|
||||||
|
if (hasValue(firstItemRD.payload)) {
|
||||||
|
const date = firstItemRD.payload.firstMetadataValue(metadataField);
|
||||||
|
if (hasValue(date) && hasValue(+date.split('-')[0])) {
|
||||||
|
lowerLimit = +date.split('-')[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const options = [];
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const oneYearBreak = Math.floor((currentYear - this.config.browseBy.oneYearLimit) / 5) * 5;
|
||||||
|
const fiveYearBreak = Math.floor((currentYear - this.config.browseBy.fiveYearLimit) / 10) * 10;
|
||||||
|
if (lowerLimit <= fiveYearBreak) {
|
||||||
|
lowerLimit -= 10;
|
||||||
|
} else if (lowerLimit <= oneYearBreak) {
|
||||||
|
lowerLimit -= 5;
|
||||||
|
} else {
|
||||||
|
lowerLimit -= 1;
|
||||||
|
}
|
||||||
|
let i = currentYear;
|
||||||
|
while (i > lowerLimit) {
|
||||||
|
options.push(i);
|
||||||
|
if (i <= fiveYearBreak) {
|
||||||
|
i -= 10;
|
||||||
|
} else if (i <= oneYearBreak) {
|
||||||
|
i -= 5;
|
||||||
|
} else {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNotEmpty(options)) {
|
||||||
|
this.startsWithOptions = options;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,10 +1,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="browse-by-metadata w-100 row">
|
<div class="browse-by-metadata w-100">
|
||||||
<ds-browse-by class="col-xs-12 w-100"
|
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + metadata | translate, value: (value)? '"' + value + '"': ''} }}"
|
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + metadata | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||||
[paginationConfig]="paginationConfig"
|
[paginationConfig]="paginationConfig"
|
||||||
[sortConfig]="sortConfig">
|
[sortConfig]="sortConfig"
|
||||||
|
[type]="startsWithType"
|
||||||
|
[startsWithOptions]="startsWithOptions"
|
||||||
|
[enableArrows]="true"
|
||||||
|
(prev)="goPrev()"
|
||||||
|
(next)="goNext()"
|
||||||
|
(pageSizeChange)="pageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="sortDirectionChange($event)">
|
||||||
</ds-browse-by>
|
</ds-browse-by>
|
||||||
|
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,6 +19,7 @@ import { SortDirection } from '../../core/cache/models/sort-options.model';
|
|||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
|
||||||
describe('BrowseByMetadataPageComponent', () => {
|
describe('BrowseByMetadataPageComponent', () => {
|
||||||
let comp: BrowseByMetadataPageComponent;
|
let comp: BrowseByMetadataPageComponent;
|
||||||
@@ -86,7 +87,8 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
{ provide: BrowseService, useValue: mockBrowseService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
|
{ provide: Router, useValue: new MockRouter() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -4,17 +4,17 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
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 { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.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 { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-metadata-page',
|
selector: 'ds-browse-by-metadata-page',
|
||||||
@@ -72,6 +72,18 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
metadata = this.defaultMetadata;
|
metadata = this.defaultMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of StartsWith options to render
|
||||||
|
* Defaults to text
|
||||||
|
*/
|
||||||
|
startsWithType = StartsWithType.text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of StartsWith options
|
||||||
|
* Should be defined after ngOnInit is called!
|
||||||
|
*/
|
||||||
|
startsWithOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value we're browing items for
|
* The value we're browing items for
|
||||||
* - When the value is not empty, we're browsing items
|
* - When the value is not empty, we're browsing items
|
||||||
@@ -79,9 +91,15 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
value = '';
|
value = '';
|
||||||
|
|
||||||
public constructor(private route: ActivatedRoute,
|
/**
|
||||||
private browseService: BrowseService,
|
* The current startsWith option (fetched and updated from query-params)
|
||||||
private dsoService: DSpaceObjectDataService) {
|
*/
|
||||||
|
startsWith: string;
|
||||||
|
|
||||||
|
public constructor(protected route: ActivatedRoute,
|
||||||
|
protected browseService: BrowseService,
|
||||||
|
protected dsoService: DSpaceObjectDataService,
|
||||||
|
protected router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -96,6 +114,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
this.metadata = params.metadata || this.defaultMetadata;
|
this.metadata = params.metadata || this.defaultMetadata;
|
||||||
this.value = +params.value || params.value || '';
|
this.value = +params.value || params.value || '';
|
||||||
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
|
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
|
||||||
if (isNotEmpty(this.value)) {
|
if (isNotEmpty(this.value)) {
|
||||||
this.updatePageWithItems(searchOptions, this.value);
|
this.updatePageWithItems(searchOptions, this.value);
|
||||||
@@ -104,6 +123,15 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
}));
|
}));
|
||||||
|
this.updateStartsWithTextOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the StartsWith options with text values
|
||||||
|
* It adds the value "0-9" as well as all letters from A to Z
|
||||||
|
*/
|
||||||
|
updateStartsWithTextOptions() {
|
||||||
|
this.startsWithOptions = ['0-9', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +172,58 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the previous page
|
||||||
|
*/
|
||||||
|
goPrev() {
|
||||||
|
if (this.items$) {
|
||||||
|
this.items$.pipe(take(1)).subscribe((items) => {
|
||||||
|
this.items$ = this.browseService.getPrevBrowseItems(items);
|
||||||
|
});
|
||||||
|
} else if (this.browseEntries$) {
|
||||||
|
this.browseEntries$.pipe(take(1)).subscribe((entries) => {
|
||||||
|
this.browseEntries$ = this.browseService.getPrevBrowseEntries(entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the next page
|
||||||
|
*/
|
||||||
|
goNext() {
|
||||||
|
if (this.items$) {
|
||||||
|
this.items$.pipe(take(1)).subscribe((items) => {
|
||||||
|
this.items$ = this.browseService.getNextBrowseItems(items);
|
||||||
|
});
|
||||||
|
} else if (this.browseEntries$) {
|
||||||
|
this.browseEntries$.pipe(take(1)).subscribe((entries) => {
|
||||||
|
this.browseEntries$ = this.browseService.getNextBrowseEntries(entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the page size
|
||||||
|
* @param size
|
||||||
|
*/
|
||||||
|
pageSizeChange(size) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: Object.assign({ pageSize: size }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the sorting direction
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
sortDirectionChange(direction) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: Object.assign({ sortDirection: direction }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
@@ -177,6 +257,7 @@ export function browseParamsToOptions(params: any,
|
|||||||
field: params.sortField || sortConfig.field
|
field: params.sortField || sortConfig.field
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
+params.startsWith || params.startsWith,
|
||||||
params.scope
|
params.scope
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
<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: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.title' | translate, value: ''} }}"
|
|
||||||
[objects$]="items$"
|
|
||||||
[paginationConfig]="paginationConfig"
|
|
||||||
[sortConfig]="sortConfig">
|
|
||||||
</ds-browse-by>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,5 +1,5 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
@@ -15,6 +15,8 @@ import { ItemDataService } from '../../core/data/item-data.service';
|
|||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
|
||||||
describe('BrowseByTitlePageComponent', () => {
|
describe('BrowseByTitlePageComponent', () => {
|
||||||
let comp: BrowseByTitlePageComponent;
|
let comp: BrowseByTitlePageComponent;
|
||||||
@@ -44,8 +46,9 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockItemDataService = {
|
const mockBrowseService = {
|
||||||
findAll: () => toRemoteData(mockItems)
|
getBrowseItemsFor: () => toRemoteData(mockItems),
|
||||||
|
getBrowseEntriesFor: () => toRemoteData([])
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockDsoService = {
|
const mockDsoService = {
|
||||||
@@ -53,7 +56,8 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({})
|
params: observableOf({}),
|
||||||
|
data: observableOf({ metadata: 'title' })
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -62,8 +66,9 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
|
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
|
{ provide: Router, useValue: new MockRouter() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,107 +1,51 @@
|
|||||||
import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } 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 { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
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 {
|
||||||
import { browseParamsToOptions } from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
BrowseByMetadataPageComponent,
|
||||||
|
browseParamsToOptions
|
||||||
|
} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
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 { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
styleUrls: ['./browse-by-title-page.component.scss'],
|
styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
|
||||||
templateUrl: './browse-by-title-page.component.html'
|
templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component for browsing items by title (dc.title)
|
* Component for browsing items by title (dc.title)
|
||||||
*/
|
*/
|
||||||
export class BrowseByTitlePageComponent implements OnInit {
|
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
|
|
||||||
|
public constructor(protected route: ActivatedRoute,
|
||||||
|
protected browseService: BrowseService,
|
||||||
|
protected dsoService: DSpaceObjectDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(route, browseService, dsoService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.route.params,
|
this.route.params,
|
||||||
this.route.queryParams,
|
this.route.queryParams,
|
||||||
(params, queryParams, ) => {
|
this.route.data,
|
||||||
return Object.assign({}, params, queryParams);
|
(params, queryParams, data ) => {
|
||||||
|
return Object.assign({}, params, queryParams, data);
|
||||||
})
|
})
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
this.updatePage(browseParamsToOptions(params, this.paginationConfig, this.sortConfig));
|
this.metadata = params.metadata || this.defaultMetadata;
|
||||||
|
this.updatePageWithItems(browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata), undefined);
|
||||||
this.updateParent(params.scope)
|
this.updateParent(params.scope)
|
||||||
}));
|
}));
|
||||||
}
|
this.updateStartsWithTextOptions();
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the current page with searchOptions
|
|
||||||
* @param searchOptions Options to narrow down your search:
|
|
||||||
* { pagination: PaginationComponentOptions,
|
|
||||||
* sort: SortOptions }
|
|
||||||
*/
|
|
||||||
updatePage(searchOptions: BrowseEntrySearchOptions) {
|
|
||||||
this.items$ = this.itemDataService.findAll({
|
|
||||||
currentPage: searchOptions.pagination.currentPage,
|
|
||||||
elementsPerPage: searchOptions.pagination.pageSize,
|
|
||||||
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 {
|
||||||
|
58
src/app/+browse-by/browse-by-guard.ts
Normal file
58
src/app/+browse-by/browse-by-guard.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A guard taking care of the correct route.data being set for the Browse-By components
|
||||||
|
*/
|
||||||
|
export class BrowseByGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(protected dsoService: DSpaceObjectDataService,
|
||||||
|
protected translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
const title = route.data.title;
|
||||||
|
const metadata = route.params.metadata || route.queryParams.metadata || route.data.metadata;
|
||||||
|
const metadataField = route.data.metadataField;
|
||||||
|
const scope = route.queryParams.scope;
|
||||||
|
const value = route.queryParams.value;
|
||||||
|
|
||||||
|
const metadataTranslated$ = this.translate.get('browse.metadata.' + metadata).pipe(take(1));
|
||||||
|
|
||||||
|
if (hasValue(scope)) {
|
||||||
|
const dsoAndMetadata$ = observableCombineLatest(metadataTranslated$, this.dsoService.findById(scope).pipe(getSucceededRemoteData()));
|
||||||
|
return dsoAndMetadata$.pipe(
|
||||||
|
map(([metadataTranslated, dsoRD]) => {
|
||||||
|
const name = dsoRD.payload.name;
|
||||||
|
route.data = this.createData(title, metadata, metadataField, name, metadataTranslated, value);;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return metadataTranslated$.pipe(
|
||||||
|
map((metadataTranslated: string) => {
|
||||||
|
route.data = this.createData(title, metadata, metadataField, '', metadataTranslated, value);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createData(title, metadata, metadataField, collection, field, value) {
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
metadata: metadata,
|
||||||
|
metadataField: metadataField,
|
||||||
|
collection: collection,
|
||||||
|
field: field,
|
||||||
|
value: hasValue(value) ? `"${value}"` : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,15 @@ 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 { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||||
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: 'title', component: BrowseByTitlePageComponent },
|
{ path: 'title', component: BrowseByTitlePageComponent, canActivate: [BrowseByGuard], data: { metadata: 'title', title: 'browse.title' } },
|
||||||
{ path: ':metadata', component: BrowseByMetadataPageComponent }
|
{ path: 'dateissued', component: BrowseByDatePageComponent, canActivate: [BrowseByGuard], data: { metadata: 'dateissued', metadataField: 'dc.date.issued', title: 'browse.title' } },
|
||||||
|
{ path: ':metadata', component: BrowseByMetadataPageComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -6,6 +6,8 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { BrowseByRoutingModule } from './browse-by-routing.module';
|
import { BrowseByRoutingModule } from './browse-by-routing.module';
|
||||||
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';
|
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||||
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,11 +17,13 @@ import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
BrowseByMetadataPageComponent
|
BrowseByMetadataPageComponent,
|
||||||
|
BrowseByDatePageComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
BrowseService
|
BrowseService,
|
||||||
|
BrowseByGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BrowseByModule {
|
export class BrowseByModule {
|
||||||
|
@@ -12,6 +12,7 @@ export class BrowseEntrySearchOptions {
|
|||||||
constructor(public metadataDefinition: string,
|
constructor(public metadataDefinition: string,
|
||||||
public pagination?: PaginationComponentOptions,
|
public pagination?: PaginationComponentOptions,
|
||||||
public sort?: SortOptions,
|
public sort?: SortOptions,
|
||||||
|
public startsWith?: string,
|
||||||
public scope?: string) {
|
public scope?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -281,4 +281,38 @@ describe('BrowseService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getFirstItemFor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
|
.returnValue(hot('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}}));
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getFirstItemFor is called with a valid browse definition id', () => {
|
||||||
|
const expectedURL = browseDefinitions[1]._links.items + '?page=0&size=1';
|
||||||
|
|
||||||
|
it('should configure a new BrowseItemsRequest', () => {
|
||||||
|
const expected = new BrowseItemsRequest(requestService.generateRequestId(), expectedURL);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getFirstItemFor(browseDefinitions[1].id);
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
|
import { distinctUntilChanged, map, startWith, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
ensureArrayHasValue,
|
ensureArrayHasValue, hasValue,
|
||||||
hasValueOperator,
|
hasValueOperator,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
isNotEmptyOperator
|
isNotEmptyOperator
|
||||||
} from '../../shared/empty.util';
|
} from '../../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { SortOptions } from '../cache/models/sort-options.model';
|
|
||||||
import { GenericSuccessResponse } from '../cache/response.models';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import {
|
import {
|
||||||
@@ -26,17 +23,19 @@ import { BrowseEntry } from '../shared/browse-entry.model';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses,
|
filterSuccessfulResponses, getBrowseDefinitionLinks, getFirstOccurrence,
|
||||||
getBrowseDefinitionLinks,
|
getRemoteDataPayload,
|
||||||
getRemoteDataPayload, getRequestFromRequestHref
|
getRequestFromRequestHref
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { Item } from '../shared/item.model';
|
import { 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';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
|
import { GenericSuccessResponse } from '../cache/response.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all actions that have to do with browse.
|
* The service handling all browse requests
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
@@ -62,6 +61,9 @@ export class BrowseService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all BrowseDefinitions
|
||||||
|
*/
|
||||||
getBrowseDefinitions(): Observable<RemoteData<BrowseDefinition[]>> {
|
getBrowseDefinitions(): Observable<RemoteData<BrowseDefinition[]>> {
|
||||||
const request$ = this.halService.getEndpoint(this.linkPath).pipe(
|
const request$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
@@ -84,8 +86,12 @@ export class BrowseService {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all BrowseEntries filtered or modified by BrowseEntrySearchOptions
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
return this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(options.metadataDefinition),
|
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.entries),
|
map((_links: any) => _links.entries),
|
||||||
@@ -93,7 +99,7 @@ export class BrowseService {
|
|||||||
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)) {
|
if (isNotEmpty(options.scope)) {
|
||||||
args.push(`scope=${options.scope}`);
|
args.push(`scope=${options.scope}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
@@ -103,49 +109,33 @@ export class BrowseService {
|
|||||||
args.push(`page=${options.pagination.currentPage - 1}`);
|
args.push(`page=${options.pagination.currentPage - 1}`);
|
||||||
args.push(`size=${options.pagination.pageSize}`);
|
args.push(`size=${options.pagination.pageSize}`);
|
||||||
}
|
}
|
||||||
|
if (isNotEmpty(options.startsWith)) {
|
||||||
|
args.push(`startsWith=${options.startsWith}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
map((endpointURL: string) => new BrowseEntriesRequest(this.requestService.generateRequestId(), endpointURL)),
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
configureRequest(this.requestService)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
|
||||||
|
|
||||||
const payload$ = requestEntry$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
|
||||||
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
|
||||||
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
|
||||||
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
|
||||||
})),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all items linked to a certain metadata value
|
* Get all items linked to a certain metadata value
|
||||||
* @param {string} definitionID definition ID to define the metadata-field (e.g. author)
|
|
||||||
* @param {string} filterValue metadata value to filter by (e.g. author's name)
|
* @param {string} filterValue metadata value to filter by (e.g. author's name)
|
||||||
* @param options Options to narrow down your search:
|
* @param options Options to narrow down your search
|
||||||
* { pagination: PaginationComponentOptions,
|
|
||||||
* sort: SortOptions }
|
|
||||||
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
||||||
*/
|
*/
|
||||||
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
return this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(options.metadataDefinition),
|
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)) {
|
if (isNotEmpty(options.scope)) {
|
||||||
args.push(`scope=${options.scope}`);
|
args.push(`scope=${options.scope}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
@@ -155,6 +145,9 @@ export class BrowseService {
|
|||||||
args.push(`page=${options.pagination.currentPage - 1}`);
|
args.push(`page=${options.pagination.currentPage - 1}`);
|
||||||
args.push(`size=${options.pagination.pageSize}`);
|
args.push(`size=${options.pagination.pageSize}`);
|
||||||
}
|
}
|
||||||
|
if (isNotEmpty(options.startsWith)) {
|
||||||
|
args.push(`startsWith=${options.startsWith}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(filterValue)) {
|
if (isNotEmpty(filterValue)) {
|
||||||
args.push(`filterValue=${filterValue}`);
|
args.push(`filterValue=${filterValue}`);
|
||||||
}
|
}
|
||||||
@@ -163,26 +156,83 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)),
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
configureRequest(this.requestService)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
|
||||||
|
|
||||||
const payload$ = requestEntry$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
|
||||||
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
|
||||||
map((list: PaginatedList<Item>) => Object.assign(list, {
|
|
||||||
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
|
||||||
})),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first item for a metadata definition in an optional scope
|
||||||
|
* @param definition
|
||||||
|
* @param scope
|
||||||
|
*/
|
||||||
|
getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> {
|
||||||
|
return this.getBrowseDefinitions().pipe(
|
||||||
|
getBrowseDefinitionLinks(definition),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((_links: any) => _links.items),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((href: string) => {
|
||||||
|
const args = [];
|
||||||
|
if (hasValue(scope)) {
|
||||||
|
args.push(`scope=${scope}`);
|
||||||
|
}
|
||||||
|
args.push('page=0');
|
||||||
|
args.push('size=1');
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
}
|
||||||
|
return href;
|
||||||
|
}),
|
||||||
|
getBrowseItemsFor(this.requestService, this.rdb),
|
||||||
|
getFirstOccurrence()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous page of items using the paginated list's prev link
|
||||||
|
* @param items
|
||||||
|
*/
|
||||||
|
getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
|
return observableOf(items.payload.prev).pipe(
|
||||||
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next page of items using the paginated list's next link
|
||||||
|
* @param items
|
||||||
|
*/
|
||||||
|
getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
|
return observableOf(items.payload.next).pipe(
|
||||||
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous page of browse-entries using the paginated list's prev link
|
||||||
|
* @param entries
|
||||||
|
*/
|
||||||
|
getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
|
return observableOf(entries.payload.prev).pipe(
|
||||||
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next page of browse-entries using the paginated list's next link
|
||||||
|
* @param entries
|
||||||
|
*/
|
||||||
|
getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
|
return observableOf(entries.payload.next).pipe(
|
||||||
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the browse URL by providing a metadatum key and linkPath
|
||||||
|
* @param metadatumKey
|
||||||
|
* @param linkPath
|
||||||
|
*/
|
||||||
getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
|
getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
|
||||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
|
||||||
return this.getBrowseDefinitions().pipe(
|
return this.getBrowseDefinitions().pipe(
|
||||||
@@ -206,3 +256,79 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a href into a PaginatedList of BrowseEntries
|
||||||
|
* @param requestService
|
||||||
|
* @param responseCache
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<string>): Observable<RemoteData<PaginatedList<BrowseEntry>>> =>
|
||||||
|
source.pipe(
|
||||||
|
map((href: string) => new BrowseEntriesRequest(requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(requestService),
|
||||||
|
toRDPaginatedBrowseEntries(requestService, rdb)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a href into a PaginatedList of Items
|
||||||
|
* @param requestService
|
||||||
|
* @param responseCache
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const getBrowseItemsFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<string>): Observable<RemoteData<PaginatedList<Item>>> =>
|
||||||
|
source.pipe(
|
||||||
|
map((href: string) => new BrowseItemsRequest(requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(requestService),
|
||||||
|
toRDPaginatedBrowseItems(requestService, rdb)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a RestRequest into a PaginatedList of Items
|
||||||
|
* @param requestService
|
||||||
|
* @param responseCache
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const toRDPaginatedBrowseItems = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<Item>>> => {
|
||||||
|
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
|
||||||
|
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
|
map((list: PaginatedList<Item>) => Object.assign(list, {
|
||||||
|
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
||||||
|
})),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a RestRequest into a PaginatedList of BrowseEntries
|
||||||
|
* @param requestService
|
||||||
|
* @param responseCache
|
||||||
|
* @param rdb
|
||||||
|
*/
|
||||||
|
export const toRDPaginatedBrowseEntries = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
|
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<BrowseEntry>>> => {
|
||||||
|
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
|
||||||
|
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
|
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
||||||
|
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
||||||
|
})),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import {
|
import {
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
@@ -15,6 +15,7 @@ import { ResponseParsingService } from './parsing.service';
|
|||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
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 { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
|
||||||
@@ -42,9 +43,11 @@ export class BrowseItemsResponseParsingService extends BaseResponseParsingServic
|
|||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
||||||
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
const serializer = new DSpaceRESTv2Serializer(DSpaceObject);
|
const serializer = new DSpaceRESTv2Serializer(NormalizedDSpaceObject);
|
||||||
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
|
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
|
||||||
|
} else if (hasValue(data.payload) && hasValue(data.payload.page)) {
|
||||||
|
return new GenericSuccessResponse([], data.statusCode, this.processPageInfo(data.payload));
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@@ -81,7 +81,7 @@ export class MetadataService {
|
|||||||
this.clearMetaTags();
|
this.clearMetaTags();
|
||||||
}
|
}
|
||||||
if (routeInfo.data.value.title) {
|
if (routeInfo.data.value.title) {
|
||||||
this.translate.get(routeInfo.data.value.title).pipe(take(1)).subscribe((translatedTitle: string) => {
|
this.translate.get(routeInfo.data.value.title, routeInfo.data.value).pipe(take(1)).subscribe((translatedTitle: string) => {
|
||||||
this.addMetaTag('title', translatedTitle);
|
this.addMetaTag('title', translatedTitle);
|
||||||
this.title.setTitle(translatedTitle);
|
this.title.setTitle(translatedTitle);
|
||||||
});
|
});
|
||||||
|
@@ -91,7 +91,7 @@ export const getBrowseDefinitionLinks = (definitionID: string) =>
|
|||||||
source.pipe(
|
source.pipe(
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
|
.find((def: BrowseDefinition) => def.id === definitionID)
|
||||||
),
|
),
|
||||||
map((def: BrowseDefinition) => {
|
map((def: BrowseDefinition) => {
|
||||||
if (isNotEmpty(def)) {
|
if (isNotEmpty(def)) {
|
||||||
@@ -101,3 +101,12 @@ export const getBrowseDefinitionLinks = (definitionID: string) =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first occurrence of an object within a paginated list
|
||||||
|
*/
|
||||||
|
export const getFirstOccurrence = () =>
|
||||||
|
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<T>>>): Observable<RemoteData<T>> =>
|
||||||
|
source.pipe(
|
||||||
|
map((rd) => Object.assign(rd, { payload: rd.payload.page.length > 0 ? rd.payload.page[0] : undefined }))
|
||||||
|
);
|
||||||
|
@@ -73,6 +73,17 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/browse/title'
|
link: '/browse/title'
|
||||||
} as LinkMenuItemModel,
|
} 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: '/browse/dateissued'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'browse_global_by_author',
|
id: 'browse_global_by_author',
|
||||||
parentID: 'browse_global',
|
parentID: 'browse_global',
|
||||||
|
@@ -1,11 +1,38 @@
|
|||||||
<ng-container *ngVar="(objects$ | async) as objects">
|
<ng-container *ngVar="(objects$ | async) as objects">
|
||||||
<h2 class="w-100">{{title | translate}}</h2>
|
<h2 class="w-100">{{title | translate}}</h2>
|
||||||
|
<ng-container *ngComponentOutlet="getStartsWithComponent(); injector: objectInjector;"></ng-container>
|
||||||
<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
|
<div *ngIf="!enableArrows">
|
||||||
[config]="paginationConfig"
|
<ds-viewable-collection
|
||||||
[sortConfig]="sortConfig"
|
[config]="paginationConfig"
|
||||||
[objects]="objects">
|
[sortConfig]="sortConfig"
|
||||||
</ds-viewable-collection>
|
[objects]="objects">
|
||||||
|
</ds-viewable-collection>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="enableArrows">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">
|
||||||
|
<button class="btn btn-outline-primary" id="paginationControls" ngbDropdownToggle><i class="fas fa-cog" aria-hidden="true"></i></button>
|
||||||
|
<div id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" ngbDropdownMenu>
|
||||||
|
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
||||||
|
<button class="dropdown-item page-size-change" *ngFor="let item of paginationConfig?.pageSizeOptions" (click)="doPageSizeChange(item)"><i [ngClass]="{'invisible': item != paginationConfig?.pageSize}" class="fas fa-check" aria-hidden="true"></i> {{item}} </button>
|
||||||
|
<h6 class="dropdown-header">{{ 'pagination.sort-direction' | translate}}</h6>
|
||||||
|
<button class="dropdown-item sort-direction-change" *ngFor="let direction of (sortDirections | dsKeys)" (click)="doSortDirectionChange(direction.value)"><i [ngClass]="{'invisible': direction.value !== sortConfig?.direction}" class="fas fa-check" aria-hidden="true"></i> {{'sorting.' + direction.key | translate}} </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li *ngFor="let object of objects?.payload?.page" class="mt-4 mb-4">
|
||||||
|
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button id="nav-prev" type="button" class="btn btn-outline-secondary float-left" (click)="goPrev()" [disabled]="objects?.payload?.currentPage <= 1"><<</button>
|
||||||
|
<button id="nav-next" type="button" class="btn btn-outline-secondary float-right" (click)="goNext()" [disabled]="objects?.payload?.currentPage >= objects?.payload?.totalPages">>></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-loading *ngIf="!objects || objects?.isLoading" 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>
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,19 +1,68 @@
|
|||||||
import { BrowseByComponent } from './browse-by.component';
|
import { BrowseByComponent } from './browse-by.component';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { SharedModule } from '../shared.module';
|
import { SharedModule } from '../shared.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { MockTranslateLoader } from '../mocks/mock-translate-loader';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
describe('BrowseByComponent', () => {
|
describe('BrowseByComponent', () => {
|
||||||
let comp: BrowseByComponent;
|
let comp: BrowseByComponent;
|
||||||
let fixture: ComponentFixture<BrowseByComponent>;
|
let fixture: ComponentFixture<BrowseByComponent>;
|
||||||
|
|
||||||
|
const mockItems = [
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'fakeId-1',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'First Fake Title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'fakeId-2',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'Second Fake Title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const mockItemsRD$ = observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItems)));
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), SharedModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
SharedModule,
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
StoreModule.forRoot({}),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
providers: [],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
@@ -41,4 +90,67 @@ describe('BrowseByComponent', () => {
|
|||||||
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined();
|
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when enableArrows is true and objects are defined', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.enableArrows = true;
|
||||||
|
comp.objects$ = mockItemsRD$;
|
||||||
|
comp.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'test-pagination',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSizeOptions: [5,10,15,20],
|
||||||
|
pageSize: 15
|
||||||
|
});
|
||||||
|
comp.sortConfig = Object.assign(new SortOptions('dc.title', SortDirection.ASC));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking the previous arrow button', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp.prev, 'emit');
|
||||||
|
fixture.debugElement.query(By.css('#nav-prev')).triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a signal to the EventEmitter',() => {
|
||||||
|
expect(comp.prev.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking the next arrow button', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp.next, 'emit');
|
||||||
|
fixture.debugElement.query(By.css('#nav-next')).triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a signal to the EventEmitter',() => {
|
||||||
|
expect(comp.next.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking a different page size', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp.pageSizeChange, 'emit');
|
||||||
|
fixture.debugElement.query(By.css('.page-size-change')).triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a signal to the EventEmitter',() => {
|
||||||
|
expect(comp.pageSizeChange.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking a different sort direction', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp.sortDirectionChange, 'emit');
|
||||||
|
fixture.debugElement.query(By.css('.sort-direction-change')).triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a signal to the EventEmitter',() => {
|
||||||
|
expect(comp.sortDirectionChange.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, 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 { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
|
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by',
|
selector: 'ds-browse-by',
|
||||||
@@ -19,7 +20,7 @@ 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 implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The i18n message to display as title
|
* The i18n message to display as title
|
||||||
*/
|
*/
|
||||||
@@ -39,4 +40,106 @@ export class BrowseByComponent {
|
|||||||
* The sorting configuration used for the list
|
* The sorting configuration used for the list
|
||||||
*/
|
*/
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of StartsWith options used to define what component to render for the options
|
||||||
|
* Defaults to text
|
||||||
|
*/
|
||||||
|
@Input() type = StartsWithType.text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of options to render for the StartsWith component
|
||||||
|
*/
|
||||||
|
@Input() startsWithOptions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
|
||||||
|
*/
|
||||||
|
@Input() enableArrows = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enableArrows is set to true, should it hide the options gear?
|
||||||
|
*/
|
||||||
|
@Input() hideGear = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enableArrows is set to true, emit when the previous button is clicked
|
||||||
|
*/
|
||||||
|
@Output() prev = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enableArrows is set to true, emit when the next button is clicked
|
||||||
|
*/
|
||||||
|
@Output() next = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enableArrows is set to true, emit when the page size is changed
|
||||||
|
*/
|
||||||
|
@Output() pageSizeChange = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enableArrows is set to true, emit when the sort direction is changed
|
||||||
|
*/
|
||||||
|
@Output() sortDirectionChange = new EventEmitter<SortDirection>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object injector used to inject the startsWithOptions to the switchable StartsWith component
|
||||||
|
*/
|
||||||
|
objectInjector: Injector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare SortDirection enumeration to use it in the template
|
||||||
|
*/
|
||||||
|
public sortDirections = SortDirection;
|
||||||
|
|
||||||
|
public constructor(private injector: Injector) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the previous page
|
||||||
|
*/
|
||||||
|
goPrev() {
|
||||||
|
this.prev.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the next page
|
||||||
|
*/
|
||||||
|
goNext() {
|
||||||
|
this.next.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the page size
|
||||||
|
* @param size
|
||||||
|
*/
|
||||||
|
doPageSizeChange(size) {
|
||||||
|
this.paginationConfig.pageSize = size;
|
||||||
|
this.pageSizeChange.emit(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the sort direction
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
doSortDirectionChange(direction) {
|
||||||
|
this.sortConfig.direction = direction;
|
||||||
|
this.sortDirectionChange.emit(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the switchable StartsWith component dependant on the type
|
||||||
|
*/
|
||||||
|
getStartsWithComponent() {
|
||||||
|
return getStartsWithComponent(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.objectInjector = Injector.create({
|
||||||
|
providers: [{ provide: 'startsWithOptions', useFactory: () => (this.startsWithOptions), deps:[] }],
|
||||||
|
parent: this.injector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
<h3>{{'browse.comcol.head' | translate}}</h3>
|
<h3>{{'browse.comcol.head' | translate}}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a [routerLink]="['/browse/title']" [queryParams]="{scope: id}">{{'browse.comcol.by.title' | translate}}</a></li>
|
<li><a [routerLink]="['/browse/title']" [queryParams]="{scope: id}">{{'browse.comcol.by.title' | translate}}</a></li>
|
||||||
|
<li><a [routerLink]="['/browse/dateissued']" [queryParams]="{scope: id}">{{'browse.comcol.by.dateissued' | translate}}</a></li>
|
||||||
<li><a [routerLink]="['/browse/author']" [queryParams]="{scope: id}">{{'browse.comcol.by.author' | 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>
|
<li><a [routerLink]="['/browse/subject']" [queryParams]="{scope: id}">{{'browse.comcol.by.subject' | translate}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<a [routerLink]="" [queryParams]="{value: object.value}" [queryParamsHandling]="'merge'" class="lead">
|
<a [routerLink]="" [queryParams]="{value: object.value, startsWith: undefined}" [queryParamsHandling]="'merge'" class="lead">
|
||||||
{{object.value}}
|
{{object.value}}
|
||||||
</a>
|
</a>
|
||||||
<span class="pr-2"> </span>
|
<span class="pr-2"> </span>
|
||||||
|
@@ -109,6 +109,8 @@ import { ObjectValuesPipe } from './utils/object-values-pipe';
|
|||||||
import { InListValidator } from './utils/in-list-validator.directive';
|
import { InListValidator } from './utils/in-list-validator.directive';
|
||||||
import { AutoFocusDirective } from './utils/auto-focus.directive';
|
import { AutoFocusDirective } from './utils/auto-focus.directive';
|
||||||
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
|
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
|
||||||
|
import { StartsWithDateComponent } from './starts-with/date/starts-with-date.component';
|
||||||
|
import { StartsWithTextComponent } from './starts-with/text/starts-with-text.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -219,6 +221,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
JournalVolumeListElementComponent,
|
JournalVolumeListElementComponent,
|
||||||
JournalIssueListElementComponent,
|
JournalIssueListElementComponent,
|
||||||
BrowseEntryListElementComponent,
|
BrowseEntryListElementComponent,
|
||||||
|
StartsWithDateComponent,
|
||||||
|
StartsWithTextComponent,
|
||||||
PlainTextMetadataListElementComponent,
|
PlainTextMetadataListElementComponent,
|
||||||
ItemMetadataListElementComponent,
|
ItemMetadataListElementComponent,
|
||||||
MetadataRepresentationListElementComponent
|
MetadataRepresentationListElementComponent
|
||||||
|
@@ -0,0 +1,34 @@
|
|||||||
|
<form class="w-100" [formGroup]="formData" (ngSubmit)="submitForm(formData.value)">
|
||||||
|
<div class="row">
|
||||||
|
<span class="col-12 font-weight-bold mb-1">
|
||||||
|
{{'browse.startsWith.jump' | translate}}
|
||||||
|
</span>
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<select id="year-select" class="form-control" (change)="setStartsWithYearEvent($event)">
|
||||||
|
<option [value]="-1" [selected]="!startsWithYear">
|
||||||
|
{{'browse.startsWith.choose_year' | translate}}
|
||||||
|
</option>
|
||||||
|
<option *ngFor="let option of startsWithOptions"
|
||||||
|
[value]="option"
|
||||||
|
[selected]="option === startsWithYear ? 'selected': null">
|
||||||
|
{{option}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-2">
|
||||||
|
<select id="month-select" class="form-control" (change)="setStartsWithMonthEvent($event)">
|
||||||
|
<option *ngFor="let option of monthOptions"
|
||||||
|
[value]="option"
|
||||||
|
[selected]="option === startsWithMonth ? 'selected': null">
|
||||||
|
{{'browse.startsWith.months.' + option | translate}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group input-group col-12 col-md-6 pt-1 pt-md-0">
|
||||||
|
<input class="form-control" placeholder="{{'browse.startsWith.type_date' | translate}}" type="text" name="startsWith" formControlName="startsWith" [value]="getStartsWith() ? getStartsWith() : ''" />
|
||||||
|
<span class="input-group-append">
|
||||||
|
<button class="btn btn-secondary" type="submit">{{'browse.startsWith.submit' | translate}}</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,7 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
// temporary fix for bootstrap 4 beta btn color issue
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: $input-bg;
|
||||||
|
color: $input-color;
|
||||||
|
}
|
@@ -0,0 +1,183 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
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 { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { StartsWithDateComponent } from './starts-with-date.component';
|
||||||
|
import { ActivatedRouteStub } from '../../testing/active-router-stub';
|
||||||
|
import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
|
||||||
|
import { RouterStub } from '../../testing/router-stub';
|
||||||
|
|
||||||
|
describe('StartsWithDateComponent', () => {
|
||||||
|
let comp: StartsWithDateComponent;
|
||||||
|
let fixture: ComponentFixture<StartsWithDateComponent>;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
const options = [2019, 2018, 2017, 2016, 2015];
|
||||||
|
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({}),
|
||||||
|
queryParams: observableOf({})
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [StartsWithDateComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'startsWithOptions', useValue: options },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: Router, useValue: new RouterStub() }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StartsWithDateComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
route = (comp as any).route;
|
||||||
|
router = (comp as any).router;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a FormGroup containing a startsWith FormControl', () => {
|
||||||
|
expect(comp.formData.value.startsWith).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting the first option in the year dropdown', () => {
|
||||||
|
let select;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
select = fixture.debugElement.query(By.css('select#year-select')).nativeElement;
|
||||||
|
select.value = select.options[0].value;
|
||||||
|
select.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to undefined', () => {
|
||||||
|
expect(comp.startsWith).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a startsWith query parameter', () => {
|
||||||
|
route.queryParams.subscribe((params) => {
|
||||||
|
expect(params.startsWith).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting the second option in the year dropdown', () => {
|
||||||
|
let select;
|
||||||
|
let input;
|
||||||
|
let expectedValue;
|
||||||
|
let extras;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
expectedValue = '' + options[0];
|
||||||
|
extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
select = fixture.debugElement.query(By.css('select#year-select')).nativeElement;
|
||||||
|
input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
select.value = select.options[1].value;
|
||||||
|
select.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the correct value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and selecting the first option in the month dropdown', () => {
|
||||||
|
let monthSelect;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
monthSelect = fixture.debugElement.query(By.css('select#month-select')).nativeElement;
|
||||||
|
monthSelect.value = monthSelect.options[0].value;
|
||||||
|
monthSelect.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the correct value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and selecting the second option in the month dropdown', () => {
|
||||||
|
let monthSelect;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
expectedValue = `${options[0]}-01`;
|
||||||
|
extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
monthSelect = fixture.debugElement.query(By.css('select#month-select')).nativeElement;
|
||||||
|
monthSelect.value = monthSelect.options[1].value;
|
||||||
|
monthSelect.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the correct value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when filling in the input form', () => {
|
||||||
|
let form;
|
||||||
|
const expectedValue = '2015';
|
||||||
|
const extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
form = fixture.debugElement.query(By.css('form'));
|
||||||
|
comp.formData.value.startsWith = expectedValue;
|
||||||
|
form.triggerEventHandler('ngSubmit', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the correct value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
140
src/app/shared/starts-with/date/starts-with-date.component.ts
Normal file
140
src/app/shared/starts-with/date/starts-with-date.component.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { renderStartsWithFor, StartsWithType } from '../starts-with-decorator';
|
||||||
|
import { StartsWithAbstractComponent } from '../starts-with-abstract.component';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A switchable component rendering StartsWith options for the type "Date".
|
||||||
|
* The options are rendered in a dropdown with an input field (of type number) next to it.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-starts-with-date',
|
||||||
|
styleUrls: ['./starts-with-date.component.scss'],
|
||||||
|
templateUrl: './starts-with-date.component.html'
|
||||||
|
})
|
||||||
|
@renderStartsWithFor(StartsWithType.date)
|
||||||
|
export class StartsWithDateComponent extends StartsWithAbstractComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of options for months to select from
|
||||||
|
*/
|
||||||
|
monthOptions: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected month
|
||||||
|
*/
|
||||||
|
startsWithMonth = 'none';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected year
|
||||||
|
*/
|
||||||
|
startsWithYear: number;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.monthOptions = [
|
||||||
|
'none',
|
||||||
|
'january',
|
||||||
|
'february',
|
||||||
|
'march',
|
||||||
|
'april',
|
||||||
|
'may',
|
||||||
|
'june',
|
||||||
|
'july',
|
||||||
|
'august',
|
||||||
|
'september',
|
||||||
|
'october',
|
||||||
|
'november',
|
||||||
|
'december'
|
||||||
|
];
|
||||||
|
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the startsWith by event
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
setStartsWithYearEvent(event: Event) {
|
||||||
|
this.startsWithYear = +(event.target as HTMLInputElement).value;
|
||||||
|
this.setStartsWithYearMonth();
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the startsWithMonth by event
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
setStartsWithMonthEvent(event: Event) {
|
||||||
|
this.startsWithMonth = (event.target as HTMLInputElement).value;
|
||||||
|
this.setStartsWithYearMonth();
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get startsWith year combined with month;
|
||||||
|
* Returned value: "{{year}}-{{month}}"
|
||||||
|
*/
|
||||||
|
getStartsWith() {
|
||||||
|
const month = this.getStartsWithMonth();
|
||||||
|
if (month > 0 && hasValue(this.startsWithYear) && this.startsWithYear !== -1) {
|
||||||
|
let twoDigitMonth = '' + month;
|
||||||
|
if (month < 10) {
|
||||||
|
twoDigitMonth = `0${month}`;
|
||||||
|
}
|
||||||
|
return `${this.startsWithYear}-${twoDigitMonth}`;
|
||||||
|
} else {
|
||||||
|
if (hasValue(this.startsWithYear) && this.startsWithYear > 0) {
|
||||||
|
return '' + this.startsWithYear;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set startsWith year combined with month;
|
||||||
|
*/
|
||||||
|
setStartsWithYearMonth() {
|
||||||
|
this.startsWith = this.getStartsWith();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the startsWith by string
|
||||||
|
* This method also sets startsWithYear and startsWithMonth correctly depending on the received value
|
||||||
|
* - When startsWith contains a "-", the first part is considered the year, the second part the month
|
||||||
|
* - When startsWith doesn't contain a "-", the whole string is expected to be the year
|
||||||
|
* startsWithMonth will be set depending on the index received after the "-"
|
||||||
|
* @param startsWith
|
||||||
|
*/
|
||||||
|
setStartsWith(startsWith: string) {
|
||||||
|
this.startsWith = startsWith;
|
||||||
|
if (hasValue(startsWith) && startsWith.indexOf('-') > -1) {
|
||||||
|
const split = startsWith.split('-');
|
||||||
|
this.startsWithYear = +split[0];
|
||||||
|
const month = +split[1];
|
||||||
|
if (month < this.monthOptions.length) {
|
||||||
|
this.startsWithMonth = this.monthOptions[month];
|
||||||
|
} else {
|
||||||
|
this.startsWithMonth = this.monthOptions[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.startsWithYear = +startsWith;
|
||||||
|
}
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get startsWithYear as a number;
|
||||||
|
*/
|
||||||
|
getStartsWithYear() {
|
||||||
|
return this.startsWithYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get startsWithMonth as a number;
|
||||||
|
*/
|
||||||
|
getStartsWithMonth() {
|
||||||
|
return this.monthOptions.indexOf(this.startsWithMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
94
src/app/shared/starts-with/starts-with-abstract.component.ts
Normal file
94
src/app/shared/starts-with/starts-with-abstract.component.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract component to render StartsWith options
|
||||||
|
*/
|
||||||
|
export class StartsWithAbstractComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* The currently selected startsWith in string format
|
||||||
|
*/
|
||||||
|
startsWith: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The formdata controlling the StartsWith input
|
||||||
|
*/
|
||||||
|
formData: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions
|
||||||
|
*/
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
public constructor(@Inject('startsWithOptions') public startsWithOptions: any[],
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(
|
||||||
|
this.route.queryParams.subscribe((params) => {
|
||||||
|
if (hasValue(params.startsWith)) {
|
||||||
|
this.setStartsWith(params.startsWith);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.formData = new FormGroup({
|
||||||
|
startsWith: new FormControl()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get startsWith
|
||||||
|
*/
|
||||||
|
getStartsWith(): any {
|
||||||
|
return this.startsWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the startsWith by event
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
setStartsWithEvent(event: Event) {
|
||||||
|
this.startsWith = (event.target as HTMLInputElement).value;
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the startsWith by string
|
||||||
|
* @param startsWith
|
||||||
|
*/
|
||||||
|
setStartsWith(startsWith: string) {
|
||||||
|
this.startsWith = startsWith;
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add/Change the url query parameter startsWith using the local variable
|
||||||
|
*/
|
||||||
|
setStartsWithParam() {
|
||||||
|
if (this.startsWith === '-1') {
|
||||||
|
this.startsWith = undefined;
|
||||||
|
}
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: Object.assign({ startsWith: this.startsWith }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the form data. Called when clicking a submit button on the form.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
submitForm(data) {
|
||||||
|
this.startsWith = data.startsWith;
|
||||||
|
this.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
13
src/app/shared/starts-with/starts-with-decorator.spec.ts
Normal file
13
src/app/shared/starts-with/starts-with-decorator.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { renderStartsWithFor, StartsWithType } from './starts-with-decorator';
|
||||||
|
|
||||||
|
describe('BrowseByStartsWithDecorator', () => {
|
||||||
|
const textDecorator = renderStartsWithFor(StartsWithType.text);
|
||||||
|
const dateDecorator = renderStartsWithFor(StartsWithType.date);
|
||||||
|
it('should have a decorator for both text and date', () => {
|
||||||
|
expect(textDecorator.length).not.toBeNull();
|
||||||
|
expect(dateDecorator.length).not.toBeNull();
|
||||||
|
});
|
||||||
|
it('should have 2 separate decorators for text and date', () => {
|
||||||
|
expect(textDecorator).not.toEqual(dateDecorator);
|
||||||
|
});
|
||||||
|
});
|
30
src/app/shared/starts-with/starts-with-decorator.ts
Normal file
30
src/app/shared/starts-with/starts-with-decorator.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const startsWithMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum that defines the type of StartsWith options
|
||||||
|
*/
|
||||||
|
export enum StartsWithType {
|
||||||
|
text = 'Text',
|
||||||
|
date = 'Date'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a decorator to render a StartsWith component for type
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
export function renderStartsWithFor(type: StartsWithType) {
|
||||||
|
return function decorator(objectElement: any) {
|
||||||
|
if (!objectElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startsWithMap.set(type, objectElement);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the correct component depending on the StartsWith type
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
export function getStartsWithComponent(type: StartsWithType) {
|
||||||
|
return startsWithMap.get(type);
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
<form class="w-100" [formGroup]="formData" (ngSubmit)="submitForm(formData.value)">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 d-none d-sm-inline-block">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li class="list-inline-item" *ngFor="let option of startsWithOptions">
|
||||||
|
<a [routerLink]="[]" (click)="setStartsWith(option)">{{option}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 col-sm-12 d-sm-none">
|
||||||
|
<select class="form-control" (change)="setStartsWithEvent($event)">
|
||||||
|
<option [value]="'-1'" [selected]="!startsWith">
|
||||||
|
{{'browse.startsWith.choose_start' | translate}}
|
||||||
|
</option>
|
||||||
|
<option *ngFor="let option of startsWithOptions"
|
||||||
|
[value]="option"
|
||||||
|
[selected]="isSelectedOption(option) ? 'selected': null">
|
||||||
|
{{option}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group input-group col-8 col-sm-12 col-md-6">
|
||||||
|
<input class="form-control" placeholder="{{'browse.startsWith.type_text' | translate}}" type="text" name="startsWith" formControlName="startsWith" [value]="getStartsWith()" />
|
||||||
|
<span class="input-group-append">
|
||||||
|
<button class="btn btn-secondary" type="submit">{{'browse.startsWith.submit' | translate}}</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,7 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
// temporary fix for bootstrap 4 beta btn color issue
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: $input-bg;
|
||||||
|
color: $input-color;
|
||||||
|
}
|
@@ -0,0 +1,178 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
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 '../../utils/enum-keys-pipe';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { StartsWithTextComponent } from './starts-with-text.component';
|
||||||
|
|
||||||
|
describe('StartsWithTextComponent', () => {
|
||||||
|
let comp: StartsWithTextComponent;
|
||||||
|
let fixture: ComponentFixture<StartsWithTextComponent>;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
const options = ['0-9', 'A', 'B', 'C'];
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [StartsWithTextComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'startsWithOptions', useValue: options }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StartsWithTextComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
route = (comp as any).route;
|
||||||
|
router = (comp as any).router;
|
||||||
|
spyOn(router, 'navigate');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a FormGroup containing a startsWith FormControl', () => {
|
||||||
|
expect(comp.formData.value.startsWith).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting the first option in the dropdown', () => {
|
||||||
|
let select;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
select = fixture.debugElement.query(By.css('select')).nativeElement;
|
||||||
|
select.value = select.options[0].value;
|
||||||
|
select.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to undefined', () => {
|
||||||
|
expect(comp.startsWith).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a startsWith query parameter', () => {
|
||||||
|
route.queryParams.subscribe((params) => {
|
||||||
|
expect(params.startsWith).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting "0-9" in the dropdown', () => {
|
||||||
|
let select;
|
||||||
|
let input;
|
||||||
|
const expectedValue = '0';
|
||||||
|
const extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
select = fixture.debugElement.query(By.css('select')).nativeElement;
|
||||||
|
input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
select.value = select.options[1].value;
|
||||||
|
select.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to "0"', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting an option in the dropdown', () => {
|
||||||
|
let select;
|
||||||
|
let input;
|
||||||
|
const expectedValue = options[1];
|
||||||
|
const extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
select = fixture.debugElement.query(By.css('select')).nativeElement;
|
||||||
|
input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
select.value = select.options[2].value;
|
||||||
|
select.dispatchEvent(new Event('change'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the expected value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking an option in the list', () => {
|
||||||
|
let optionLink;
|
||||||
|
let input;
|
||||||
|
const expectedValue = options[1];
|
||||||
|
const extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
optionLink = fixture.debugElement.query(By.css('.list-inline-item:nth-child(2) > a')).nativeElement;
|
||||||
|
input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
optionLink.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the expected value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the input field', () => {
|
||||||
|
expect(input.value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when filling in the input form', () => {
|
||||||
|
let form;
|
||||||
|
const expectedValue = 'A';
|
||||||
|
const extras = {
|
||||||
|
queryParams: Object.assign({ startsWith: expectedValue }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
form = fixture.debugElement.query(By.css('form'));
|
||||||
|
comp.formData.value.startsWith = expectedValue;
|
||||||
|
form.triggerEventHandler('ngSubmit', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set startsWith to the correct value', () => {
|
||||||
|
expect(comp.startsWith).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a startsWith query parameter', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([], extras);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,49 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { renderStartsWithFor, StartsWithType } from '../starts-with-decorator';
|
||||||
|
import { StartsWithAbstractComponent } from '../starts-with-abstract.component';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A switchable component rendering StartsWith options for the type "Text".
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-starts-with-text',
|
||||||
|
styleUrls: ['./starts-with-text.component.scss'],
|
||||||
|
templateUrl: './starts-with-text.component.html'
|
||||||
|
})
|
||||||
|
@renderStartsWithFor(StartsWithType.text)
|
||||||
|
export class StartsWithTextComponent extends StartsWithAbstractComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get startsWith as text;
|
||||||
|
*/
|
||||||
|
getStartsWith() {
|
||||||
|
if (hasValue(this.startsWith)) {
|
||||||
|
return this.startsWith;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add/Change the url query parameter startsWith using the local variable
|
||||||
|
*/
|
||||||
|
setStartsWithParam() {
|
||||||
|
if (this.startsWith === '0-9') {
|
||||||
|
this.startsWith = '0';
|
||||||
|
}
|
||||||
|
super.setStartsWithParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the provided option is equal to the current startsWith
|
||||||
|
* @param option
|
||||||
|
*/
|
||||||
|
isSelectedOption(option: string): boolean {
|
||||||
|
if (this.startsWith === '0' && option === '0-9') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return option === this.startsWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/config/browse-by-config.interface.ts
Normal file
21
src/config/browse-by-config.interface.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config that determines how the dropdown list of years are created for browse-by-date components
|
||||||
|
*/
|
||||||
|
export interface BrowseByConfig extends Config {
|
||||||
|
/**
|
||||||
|
* The max amount of years to display using jumps of one year (current year - oneYearLimit)
|
||||||
|
*/
|
||||||
|
oneYearLimit: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit for years to display using jumps of five years (current year - fiveYearLimit)
|
||||||
|
*/
|
||||||
|
fiveYearLimit: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The absolute lowest year to display in the dropdown when no lowest date can be found for all items
|
||||||
|
*/
|
||||||
|
defaultLowerLimit: number;
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import { UniversalConfig } from './universal-config.interface';
|
|||||||
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
||||||
import { FormConfig } from './form-config.interfaces';
|
import { FormConfig } from './form-config.interfaces';
|
||||||
import {LangConfig} from './lang-config.interface';
|
import {LangConfig} from './lang-config.interface';
|
||||||
|
import { BrowseByConfig } from './browse-by-config.interface';
|
||||||
import { ItemPageConfig } from './item-page-config.interface';
|
import { ItemPageConfig } from './item-page-config.interface';
|
||||||
|
|
||||||
export interface GlobalConfig extends Config {
|
export interface GlobalConfig extends Config {
|
||||||
@@ -20,5 +21,6 @@ export interface GlobalConfig extends Config {
|
|||||||
debug: boolean;
|
debug: boolean;
|
||||||
defaultLanguage: string;
|
defaultLanguage: string;
|
||||||
languages: LangConfig[];
|
languages: LangConfig[];
|
||||||
|
browseBy: BrowseByConfig;
|
||||||
item: ItemPageConfig;
|
item: ItemPageConfig;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user