mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into w2p-59334_edit-item-metadata-branch
Conflicts: src/app/+community-page/community-page.component.ts src/app/core/auth/server-auth.service.ts src/app/core/data/dso-response-parsing.service.ts src/app/core/data/item-data.service.ts src/app/core/data/metadata-schema-data.service.ts src/app/core/data/request.models.ts src/app/core/metadata/metadata.service.spec.ts src/app/core/metadata/normalized-metadata-schema.model.ts src/app/core/shared/metadatum.model.ts src/app/core/shared/operators.spec.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -388,7 +388,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browse": {
|
"browse": {
|
||||||
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
|
"title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||||
|
"metadata": {
|
||||||
|
"title": "Title",
|
||||||
|
"author": "Author",
|
||||||
|
"subject": "Subject"
|
||||||
|
},
|
||||||
|
"comcol": {
|
||||||
|
"head": "Browse",
|
||||||
|
"by": {
|
||||||
|
"title": "By Title",
|
||||||
|
"author": "By Author",
|
||||||
|
"subject": "By Subject"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"empty": "No items to show."
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"registries": {
|
"registries": {
|
||||||
@@ -519,6 +533,7 @@
|
|||||||
"browse_global_by_issue_date": "By Issue Date",
|
"browse_global_by_issue_date": "By Issue Date",
|
||||||
"browse_global_by_author": "By Author",
|
"browse_global_by_author": "By Author",
|
||||||
"browse_global_by_title": "By Title",
|
"browse_global_by_title": "By Title",
|
||||||
|
"browse_global_by_subject": "By Subject",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"browse_community": "This Community",
|
"browse_community": "This Community",
|
||||||
"browse_community_by_issue_date": "By Issue Date",
|
"browse_community_by_issue_date": "By Issue Date",
|
||||||
|
@@ -32,6 +32,11 @@ const initialState: MetadataRegistryState = {
|
|||||||
selectedFields: []
|
selectedFields: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer that handles MetadataRegistryActions to modify metadata schema and/or field states
|
||||||
|
* @param state The current MetadataRegistryState
|
||||||
|
* @param action The MetadataRegistryAction to perform on the state
|
||||||
|
*/
|
||||||
export function metadataRegistryReducer(state = initialState, action: MetadataRegistryAction): MetadataRegistryState {
|
export function metadataRegistryReducer(state = initialState, action: MetadataRegistryAction): MetadataRegistryState {
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
@@ -95,6 +95,9 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
private translateService: TranslateService) {
|
private translateService: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest(
|
combineLatest(
|
||||||
this.translateService.get(`${this.messagePrefix}.element`),
|
this.translateService.get(`${this.messagePrefix}.element`),
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="browse-by-author w-100 row">
|
|
||||||
<ds-browse-by class="col-xs-12 w-100"
|
|
||||||
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '"' + value + '"': ''} }}"
|
|
||||||
[objects$]="(items$ !== undefined)? items$ : authors$"
|
|
||||||
[currentUrl]="currentUrl"
|
|
||||||
[paginationConfig]="paginationConfig"
|
|
||||||
[sortConfig]="sortConfig">
|
|
||||||
</ds-browse-by>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,107 +0,0 @@
|
|||||||
|
|
||||||
import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
|
||||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-browse-by-author-page',
|
|
||||||
styleUrls: ['./browse-by-author-page.component.scss'],
|
|
||||||
templateUrl: './browse-by-author-page.component.html'
|
|
||||||
})
|
|
||||||
/**
|
|
||||||
* Component for browsing (items) by author (dc.contributor.author)
|
|
||||||
*/
|
|
||||||
export class BrowseByAuthorPageComponent implements OnInit {
|
|
||||||
|
|
||||||
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
|
|
||||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
|
||||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
|
||||||
id: 'browse-by-author-pagination',
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: 20
|
|
||||||
});
|
|
||||||
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
|
|
||||||
subs: Subscription[] = [];
|
|
||||||
currentUrl: string;
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.currentUrl = this.route.snapshot.pathFromRoot
|
|
||||||
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
|
|
||||||
.join('/');
|
|
||||||
this.updatePage({
|
|
||||||
pagination: this.paginationConfig,
|
|
||||||
sort: this.sortConfig
|
|
||||||
});
|
|
||||||
this.subs.push(
|
|
||||||
observableCombineLatest(
|
|
||||||
this.route.params,
|
|
||||||
this.route.queryParams,
|
|
||||||
(params, queryParams, ) => {
|
|
||||||
return Object.assign({}, params, queryParams);
|
|
||||||
})
|
|
||||||
.subscribe((params) => {
|
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
|
||||||
const sortDirection = params.sortDirection || this.sortConfig.direction;
|
|
||||||
const sortField = params.sortField || this.sortConfig.field;
|
|
||||||
this.value = +params.value || params.value || '';
|
|
||||||
const pagination = Object.assign({},
|
|
||||||
this.paginationConfig,
|
|
||||||
{ currentPage: page, pageSize: pageSize }
|
|
||||||
);
|
|
||||||
const sort = Object.assign({},
|
|
||||||
this.sortConfig,
|
|
||||||
{ direction: sortDirection, field: sortField }
|
|
||||||
);
|
|
||||||
const searchOptions = {
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
};
|
|
||||||
if (isNotEmpty(this.value)) {
|
|
||||||
this.updatePageWithItems(searchOptions, this.value);
|
|
||||||
} else {
|
|
||||||
this.updatePage(searchOptions);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the current page with searchOptions
|
|
||||||
* @param searchOptions Options to narrow down your search:
|
|
||||||
* { pagination: PaginationComponentOptions,
|
|
||||||
* sort: SortOptions }
|
|
||||||
*/
|
|
||||||
updatePage(searchOptions) {
|
|
||||||
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
|
|
||||||
this.items$ = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the current page with searchOptions and display items linked to author
|
|
||||||
* @param searchOptions Options to narrow down your search:
|
|
||||||
* { pagination: PaginationComponentOptions,
|
|
||||||
* sort: SortOptions }
|
|
||||||
* @param author The author's name for displaying items
|
|
||||||
*/
|
|
||||||
updatePageWithItems(searchOptions, author: string) {
|
|
||||||
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="browse-by-metadata w-100 row">
|
||||||
|
<ds-browse-by class="col-xs-12 w-100"
|
||||||
|
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + metadata | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||||
|
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||||
|
[paginationConfig]="paginationConfig"
|
||||||
|
[sortConfig]="sortConfig">
|
||||||
|
</ds-browse-by>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,162 @@
|
|||||||
|
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
|
describe('BrowseByMetadataPageComponent', () => {
|
||||||
|
let comp: BrowseByMetadataPageComponent;
|
||||||
|
let fixture: ComponentFixture<BrowseByMetadataPageComponent>;
|
||||||
|
let browseService: BrowseService;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
|
||||||
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
id: 'test-uuid',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'test community'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockEntries = [
|
||||||
|
{
|
||||||
|
type: 'author',
|
||||||
|
authority: null,
|
||||||
|
value: 'John Doe',
|
||||||
|
language: 'en',
|
||||||
|
count: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'author',
|
||||||
|
authority: null,
|
||||||
|
value: 'James Doe',
|
||||||
|
language: 'en',
|
||||||
|
count: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subject',
|
||||||
|
authority: null,
|
||||||
|
value: 'Fake subject',
|
||||||
|
language: 'en',
|
||||||
|
count: 2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockItems = [
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'fakeId'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockBrowseService = {
|
||||||
|
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData(mockEntries.filter((entry) => entry.type === options.metadataDefinition)),
|
||||||
|
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData(mockItems)
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDsoService = {
|
||||||
|
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||||
|
};
|
||||||
|
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({})
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BrowseByMetadataPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
browseService = (comp as any).browseService;
|
||||||
|
route = (comp as any).route;
|
||||||
|
route.params = observableOf({});
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch the correct entries depending on the metadata definition', () => {
|
||||||
|
comp.browseEntries$.subscribe((result) => {
|
||||||
|
expect(result.payload.page).toEqual(mockEntries.filter((entry) => entry.type === 'author'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fetch any items when no value is provided', () => {
|
||||||
|
expect(comp.items$).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a value is provided', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const paramsWithValue = {
|
||||||
|
metadata: 'author',
|
||||||
|
value: 'John Doe'
|
||||||
|
};
|
||||||
|
|
||||||
|
route.params = observableOf(paramsWithValue);
|
||||||
|
comp.ngOnInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch items', () => {
|
||||||
|
comp.items$.subscribe((result) => {
|
||||||
|
expect(result.payload.page).toEqual(mockItems);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling browseParamsToOptions', () => {
|
||||||
|
let result: BrowseEntrySearchOptions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const paramsWithPaginationAndScope = {
|
||||||
|
page: 5,
|
||||||
|
pageSize: 10,
|
||||||
|
sortDirection: SortDirection.ASC,
|
||||||
|
sortField: 'fake-field',
|
||||||
|
scope: 'fake-scope'
|
||||||
|
};
|
||||||
|
|
||||||
|
result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
||||||
|
expect(result.metadataDefinition).toEqual('author');
|
||||||
|
expect(result.pagination.currentPage).toEqual(5);
|
||||||
|
expect(result.pagination.pageSize).toEqual(10);
|
||||||
|
expect(result.sort.direction).toEqual(SortDirection.ASC);
|
||||||
|
expect(result.sort.field).toEqual('fake-field');
|
||||||
|
expect(result.scope).toEqual('fake-scope');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), objects)));
|
||||||
|
}
|
@@ -0,0 +1,182 @@
|
|||||||
|
import {combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-browse-by-metadata-page',
|
||||||
|
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||||
|
templateUrl: './browse-by-metadata-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for browsing (items) by metadata definition
|
||||||
|
* A metadata definition is a short term used to describe one or multiple metadata fields.
|
||||||
|
* An example would be 'author' for 'dc.contributor.*'
|
||||||
|
*/
|
||||||
|
export class BrowseByMetadataPageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of browse-entries to display
|
||||||
|
*/
|
||||||
|
browseEntries$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of items to display when a value is present
|
||||||
|
*/
|
||||||
|
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current Community or Collection we're browsing metadata/items in
|
||||||
|
*/
|
||||||
|
parent$: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination config used to display the values
|
||||||
|
*/
|
||||||
|
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'browse-by-metadata-pagination',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting config used to sort the values (defaults to Ascending)
|
||||||
|
*/
|
||||||
|
sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions
|
||||||
|
*/
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default metadata definition to resort to when none is provided
|
||||||
|
*/
|
||||||
|
defaultMetadata = 'author';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current metadata definition
|
||||||
|
*/
|
||||||
|
metadata = this.defaultMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value we're browing items for
|
||||||
|
* - When the value is not empty, we're browsing items
|
||||||
|
* - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
|
||||||
|
*/
|
||||||
|
value = '';
|
||||||
|
|
||||||
|
public constructor(private route: ActivatedRoute,
|
||||||
|
private browseService: BrowseService,
|
||||||
|
private dsoService: DSpaceObjectDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest(
|
||||||
|
this.route.params,
|
||||||
|
this.route.queryParams,
|
||||||
|
(params, queryParams, ) => {
|
||||||
|
return Object.assign({}, params, queryParams);
|
||||||
|
})
|
||||||
|
.subscribe((params) => {
|
||||||
|
this.metadata = params.metadata || this.defaultMetadata;
|
||||||
|
this.value = +params.value || params.value || '';
|
||||||
|
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
|
||||||
|
if (isNotEmpty(this.value)) {
|
||||||
|
this.updatePageWithItems(searchOptions, this.value);
|
||||||
|
} else {
|
||||||
|
this.updatePage(searchOptions);
|
||||||
|
}
|
||||||
|
this.updateParent(params.scope);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current page with searchOptions
|
||||||
|
* @param searchOptions Options to narrow down your search:
|
||||||
|
* { metadata: string
|
||||||
|
* pagination: PaginationComponentOptions,
|
||||||
|
* sort: SortOptions,
|
||||||
|
* scope: string }
|
||||||
|
*/
|
||||||
|
updatePage(searchOptions: BrowseEntrySearchOptions) {
|
||||||
|
this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions);
|
||||||
|
this.items$ = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current page with searchOptions and display items linked to the given value
|
||||||
|
* @param searchOptions Options to narrow down your search:
|
||||||
|
* { metadata: string
|
||||||
|
* pagination: PaginationComponentOptions,
|
||||||
|
* sort: SortOptions,
|
||||||
|
* scope: string }
|
||||||
|
* @param value The value of the browse-entry to display items for
|
||||||
|
*/
|
||||||
|
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||||
|
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the parent Community or Collection using their scope
|
||||||
|
* @param scope The UUID of the Community or Collection to fetch
|
||||||
|
*/
|
||||||
|
updateParent(scope: string) {
|
||||||
|
if (hasValue(scope)) {
|
||||||
|
this.parent$ = this.dsoService.findById(scope).pipe(
|
||||||
|
getSucceededRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
|
||||||
|
* @param params URL and query parameters
|
||||||
|
* @param paginationConfig Pagination configuration
|
||||||
|
* @param sortConfig Sorting configuration
|
||||||
|
* @param metadata Optional metadata definition to fetch browse entries/items for
|
||||||
|
*/
|
||||||
|
export function browseParamsToOptions(params: any,
|
||||||
|
paginationConfig: PaginationComponentOptions,
|
||||||
|
sortConfig: SortOptions,
|
||||||
|
metadata?: string): BrowseEntrySearchOptions {
|
||||||
|
return new BrowseEntrySearchOptions(
|
||||||
|
metadata,
|
||||||
|
Object.assign({},
|
||||||
|
paginationConfig,
|
||||||
|
{
|
||||||
|
currentPage: +params.page || paginationConfig.currentPage,
|
||||||
|
pageSize: +params.pageSize || paginationConfig.pageSize
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Object.assign({},
|
||||||
|
sortConfig,
|
||||||
|
{
|
||||||
|
direction: params.sortDirection || sortConfig.direction,
|
||||||
|
field: params.sortField || sortConfig.field
|
||||||
|
}
|
||||||
|
),
|
||||||
|
params.scope
|
||||||
|
);
|
||||||
|
}
|
@@ -1,9 +1,8 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="browse-by-title w-100 row">
|
<div class="browse-by-title w-100 row">
|
||||||
<ds-browse-by class="col-xs-12 w-100"
|
<ds-browse-by class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
|
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.title' | translate, value: ''} }}"
|
||||||
[objects$]="items$"
|
[objects$]="items$"
|
||||||
[currentUrl]="currentUrl"
|
|
||||||
[paginationConfig]="paginationConfig"
|
[paginationConfig]="paginationConfig"
|
||||||
[sortConfig]="sortConfig">
|
[sortConfig]="sortConfig">
|
||||||
</ds-browse-by>
|
</ds-browse-by>
|
||||||
|
@@ -0,0 +1,85 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||||
|
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
|
||||||
|
describe('BrowseByTitlePageComponent', () => {
|
||||||
|
let comp: BrowseByTitlePageComponent;
|
||||||
|
let fixture: ComponentFixture<BrowseByTitlePageComponent>;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
|
||||||
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
id: 'test-uuid',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'test community'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItems = [
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'fakeId',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'Fake Title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockItemDataService = {
|
||||||
|
findAll: () => toRemoteData(mockItems)
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDsoService = {
|
||||||
|
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||||
|
};
|
||||||
|
|
||||||
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
params: observableOf({})
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BrowseByTitlePageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
itemDataService = (comp as any).itemDataService;
|
||||||
|
route = (comp as any).route;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize the list of items', () => {
|
||||||
|
comp.items$.subscribe((result) => {
|
||||||
|
expect(result.payload.page).toEqual(mockItems);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,16 +1,20 @@
|
|||||||
|
import { combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
||||||
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { browseParamsToOptions } from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
@@ -22,28 +26,44 @@ import { Collection } from '../../core/shared/collection.model';
|
|||||||
*/
|
*/
|
||||||
export class BrowseByTitlePageComponent implements OnInit {
|
export class BrowseByTitlePageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of items to display
|
||||||
|
*/
|
||||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current Community or Collection we're browsing metadata/items in
|
||||||
|
*/
|
||||||
|
parent$: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination configuration to use for displaying the list of items
|
||||||
|
*/
|
||||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'browse-by-title-pagination',
|
id: 'browse-by-title-pagination',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20
|
pageSize: 20
|
||||||
});
|
});
|
||||||
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
|
|
||||||
subs: Subscription[] = [];
|
|
||||||
currentUrl: string;
|
|
||||||
|
|
||||||
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
|
/**
|
||||||
|
* The sorting configuration to use for displaying the list of items
|
||||||
|
* Sorted by title (Ascending by default)
|
||||||
|
*/
|
||||||
|
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions
|
||||||
|
*/
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
public constructor(private itemDataService: ItemDataService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private dsoService: DSpaceObjectDataService) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.currentUrl = this.route.snapshot.pathFromRoot
|
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||||
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
|
|
||||||
.join('/');
|
|
||||||
this.updatePage({
|
|
||||||
pagination: this.paginationConfig,
|
|
||||||
sort: this.sortConfig
|
|
||||||
});
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.route.params,
|
this.route.params,
|
||||||
@@ -52,22 +72,8 @@ export class BrowseByTitlePageComponent implements OnInit {
|
|||||||
return Object.assign({}, params, queryParams);
|
return Object.assign({}, params, queryParams);
|
||||||
})
|
})
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
this.updatePage(browseParamsToOptions(params, this.paginationConfig, this.sortConfig));
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
this.updateParent(params.scope)
|
||||||
const sortDirection = params.sortDirection || this.sortConfig.direction;
|
|
||||||
const sortField = params.sortField || this.sortConfig.field;
|
|
||||||
const pagination = Object.assign({},
|
|
||||||
this.paginationConfig,
|
|
||||||
{ currentPage: page, pageSize: pageSize }
|
|
||||||
);
|
|
||||||
const sort = Object.assign({},
|
|
||||||
this.sortConfig,
|
|
||||||
{ direction: sortDirection, field: sortField }
|
|
||||||
);
|
|
||||||
this.updatePage({
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,14 +83,27 @@ export class BrowseByTitlePageComponent implements OnInit {
|
|||||||
* { pagination: PaginationComponentOptions,
|
* { pagination: PaginationComponentOptions,
|
||||||
* sort: SortOptions }
|
* sort: SortOptions }
|
||||||
*/
|
*/
|
||||||
updatePage(searchOptions) {
|
updatePage(searchOptions: BrowseEntrySearchOptions) {
|
||||||
this.items$ = this.itemDataService.findAll({
|
this.items$ = this.itemDataService.findAll({
|
||||||
currentPage: searchOptions.pagination.currentPage,
|
currentPage: searchOptions.pagination.currentPage,
|
||||||
elementsPerPage: searchOptions.pagination.pageSize,
|
elementsPerPage: searchOptions.pagination.pageSize,
|
||||||
sort: searchOptions.sort
|
sort: searchOptions.sort,
|
||||||
|
scopeID: searchOptions.scope
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the parent Community or Collection using their scope
|
||||||
|
* @param scope The UUID of the Community or Collection to fetch
|
||||||
|
*/
|
||||||
|
updateParent(scope: string) {
|
||||||
|
if (hasValue(scope)) {
|
||||||
|
this.parent$ = this.dsoService.findById(scope).pipe(
|
||||||
|
getSucceededRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
|
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
|
||||||
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
|
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: 'title', component: BrowseByTitlePageComponent },
|
{ path: 'title', component: BrowseByTitlePageComponent },
|
||||||
{ path: 'author', component: BrowseByAuthorPageComponent }
|
{ path: ':metadata', component: BrowseByMetadataPageComponent }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -4,8 +4,8 @@ import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-ti
|
|||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { BrowseByRoutingModule } from './browse-by-routing.module';
|
import { BrowseByRoutingModule } from './browse-by-routing.module';
|
||||||
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
|
|
||||||
import { BrowseService } from '../core/browse/browse.service';
|
import { BrowseService } from '../core/browse/browse.service';
|
||||||
|
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,7 +15,7 @@ import { BrowseService } from '../core/browse/browse.service';
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
BrowseByAuthorPageComponent
|
BrowseByMetadataPageComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
@@ -7,6 +7,8 @@
|
|||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collection.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
|
<!-- Browse-By Links -->
|
||||||
|
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
||||||
<!-- Collection logo -->
|
<!-- Collection logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||||
<!-- Community name -->
|
<!-- Community name -->
|
||||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
||||||
|
<!-- Browse-By Links -->
|
||||||
|
<ds-comcol-page-browse-by [id]="communityPayload.id"></ds-comcol-page-browse-by>
|
||||||
<!-- Community logo -->
|
<!-- Community logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
|
@@ -24,7 +24,6 @@ import { hasValue } from '../shared/empty.util';
|
|||||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||||
communityRD$: Observable<RemoteData<Community>>;
|
communityRD$: Observable<RemoteData<Community>>;
|
||||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@@ -18,42 +18,38 @@ describe('SubCommunityList Component', () => {
|
|||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
id: '123456789-1',
|
id: '123456789-1',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{ language: 'en_US', value: 'SubCommunity 1' }
|
||||||
language: 'en_US',
|
]
|
||||||
value: 'SubCommunity 1'
|
}
|
||||||
}]
|
|
||||||
}),
|
}),
|
||||||
Object.assign(new Community(), {
|
Object.assign(new Community(), {
|
||||||
id: '123456789-2',
|
id: '123456789-2',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{ language: 'en_US', value: 'SubCommunity 2' }
|
||||||
language: 'en_US',
|
]
|
||||||
value: 'SubCommunity 2'
|
}
|
||||||
}]
|
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{ language: 'en_US', value: 'Test title' }
|
||||||
language: 'en_US',
|
]
|
||||||
value: 'Test title'
|
},
|
||||||
}],
|
|
||||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||||
undefined, new PaginatedList(new PageInfo(), [])))
|
undefined, new PaginatedList(new PageInfo(), [])))
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunity = Object.assign(new Community(), {
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{ language: 'en_US', value: 'Test title' }
|
||||||
language: 'en_US',
|
]
|
||||||
value: 'Test title'
|
},
|
||||||
}],
|
|
||||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||||
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
||||||
})
|
})
|
||||||
|
@@ -7,10 +7,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let metadatum of metadata" class="metadata-row">
|
<ng-container *ngFor="let mdEntry of metadata | keyvalue">
|
||||||
<td>{{metadatum.key}}</td>
|
<tr *ngFor="let mdValue of mdEntry.value" class="metadata-row">
|
||||||
<td>{{metadatum.value}}</td>
|
<td>{{mdEntry.key}}</td>
|
||||||
<td>{{metadatum.language}}</td>
|
<td>{{mdValue.value}}</td>
|
||||||
</tr>
|
<td>{{mdValue.language}}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -11,10 +11,14 @@ const mockItem = Object.assign(new Item(), {
|
|||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
handle: 'fake/handle',
|
handle: 'fake/handle',
|
||||||
lastModified: '2018',
|
lastModified: '2018',
|
||||||
metadata: [
|
metadata: {
|
||||||
{key: 'dc.title', value: 'Mock item title', language: 'en'},
|
'dc.title': [
|
||||||
{key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
|
{ value: 'Mock item title', language: 'en' }
|
||||||
]
|
],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{ value: 'Mayer, Ed', language: '' }
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ModifyItemOverviewComponent', () => {
|
describe('ModifyItemOverviewComponent', () => {
|
||||||
@@ -37,19 +41,19 @@ describe('ModifyItemOverviewComponent', () => {
|
|||||||
const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
|
const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
|
||||||
expect(metadataRows.length).toEqual(2);
|
expect(metadataRows.length).toEqual(2);
|
||||||
|
|
||||||
const titleRow = metadataRows[0].queryAll(By.css('td'));
|
const authorRow = metadataRows[0].queryAll(By.css('td'));
|
||||||
expect(titleRow.length).toEqual(3);
|
|
||||||
|
|
||||||
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
|
|
||||||
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
|
|
||||||
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
|
|
||||||
|
|
||||||
const authorRow = metadataRows[1].queryAll(By.css('td'));
|
|
||||||
expect(authorRow.length).toEqual(3);
|
expect(authorRow.length).toEqual(3);
|
||||||
|
|
||||||
expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
|
expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
|
||||||
expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
|
expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
|
||||||
expect(authorRow[2].nativeElement.innerHTML).toEqual('');
|
expect(authorRow[2].nativeElement.innerHTML).toEqual('');
|
||||||
|
|
||||||
|
const titleRow = metadataRows[1].queryAll(By.css('td'));
|
||||||
|
expect(titleRow.length).toEqual(3);
|
||||||
|
|
||||||
|
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
|
||||||
|
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
|
||||||
|
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import {Item} from '../../../core/shared/item.model';
|
||||||
import {Metadatum} from '../../../core/shared/metadatum.model';
|
import {MetadataMap} from '../../../core/shared/metadata.interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-modify-item-overview',
|
selector: 'ds-modify-item-overview',
|
||||||
@@ -12,7 +12,7 @@ import {Metadatum} from '../../../core/shared/metadatum.model';
|
|||||||
export class ModifyItemOverviewComponent implements OnInit {
|
export class ModifyItemOverviewComponent implements OnInit {
|
||||||
|
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
metadata: Metadatum[];
|
metadata: MetadataMap;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.metadata = this.item.metadata;
|
this.metadata = this.item.metadata;
|
||||||
|
@@ -14,12 +14,14 @@ let collectionsComponent: CollectionsComponent;
|
|||||||
let fixture: ComponentFixture<CollectionsComponent>;
|
let fixture: ComponentFixture<CollectionsComponent>;
|
||||||
|
|
||||||
const mockCollection1: Collection = Object.assign(new Collection(), {
|
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, true, null, mockCollection1))});
|
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, true, null, mockCollection1))});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
<ds-metadata-field-wrapper [label]="label | translate">
|
||||||
<a *ngFor="let metadatum of values; let last=last;" [href]="metadatum.value">
|
<a *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value">
|
||||||
{{ linktext || metadatum.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
|
{{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||||
</a>
|
</a>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { MetadataValuesComponent } from '../metadata-values/metadata-values.component';
|
import { MetadataValuesComponent } from '../metadata-values/metadata-values.component';
|
||||||
|
import { MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link.
|
* This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link.
|
||||||
@@ -18,7 +19,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent {
|
|||||||
|
|
||||||
@Input() linktext: any;
|
@Input() linktext: any;
|
||||||
|
|
||||||
@Input() values: any;
|
@Input() mdValues: MetadataValue[];
|
||||||
|
|
||||||
@Input() separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
<ds-metadata-field-wrapper [label]="label | translate">
|
||||||
<span *ngFor="let metadatum of values; let last=last;">
|
<span *ngFor="let mdValue of mdValues; let last=last;">
|
||||||
{{metadatum.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
|
{{mdValue.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||||
</span>
|
</span>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
import { MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||||
@@ -12,7 +12,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
|
|||||||
})
|
})
|
||||||
export class MetadataValuesComponent {
|
export class MetadataValuesComponent {
|
||||||
|
|
||||||
@Input() values: Metadatum[];
|
@Input() mdValues: MetadataValue[];
|
||||||
|
|
||||||
@Input() separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||||
<dd class="col-md-8">{{file.findMetadata("dc.description")}}</dd>
|
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
@@ -9,11 +9,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-responsive table-striped">
|
<table class="table table-responsive table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let metadatum of (metadata$ | async)">
|
<ng-container *ngFor="let mdEntry of (metadata$ | async) | keyvalue">
|
||||||
<td>{{metadatum.key}}</td>
|
<tr *ngFor="let mdValue of mdEntry.value">
|
||||||
<td>{{metadatum.value}}</td>
|
<td>{{mdEntry.key}}</td>
|
||||||
<td>{{metadatum.language}}</td>
|
<td>{{mdValue.value}}</td>
|
||||||
</tr>
|
<td>{{mdValue.language}}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||||
|
@@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { ItemPageComponent } from '../simple/item-page.component';
|
import { ItemPageComponent } from '../simple/item-page.component';
|
||||||
import { Metadatum } from '../../core/shared/metadatum.model';
|
import { MetadataMap } from '../../core/shared/metadata.interfaces';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
@@ -34,7 +34,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
metadata$: Observable<Metadatum[]>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
||||||
super(route, items, metadataService);
|
super(route, items, metadataService);
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="item-page-specific-field">
|
<div class="item-page-specific-field">
|
||||||
<ds-metadata-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
<ds-metadata-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<h2 class="item-page-title-field">
|
<h2 class="item-page-title-field">
|
||||||
<ds-metadata-values [values]="item?.filterMetadata(fields)"></ds-metadata-values>
|
<ds-metadata-values [mdValues]="item?.allMetadata(fields)"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="item-page-specific-field">
|
<div class="item-page-specific-field">
|
||||||
<ds-metadata-uri-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
<ds-metadata-uri-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize } from 'cerialize';
|
||||||
import { Metadatum } from '../core/shared/metadatum.model';
|
import { MetadataMap } from '../core/shared/metadata.interfaces';
|
||||||
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +16,6 @@ export class NormalizedSearchResult implements ListableObject {
|
|||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
hitHighlights: Metadatum[];
|
hitHighlights: MetadataMap;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { Metadatum } from '../core/shared/metadatum.model';
|
import { MetadataMap } from '../core/shared/metadata.interfaces';
|
||||||
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +14,6 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
*/
|
*/
|
||||||
hitHighlights: Metadatum[];
|
hitHighlights: MetadataMap;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -111,33 +111,38 @@ export const objects = [
|
|||||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description': [
|
||||||
key: 'dc.description',
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: ''
|
value: ''
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.description.abstract',
|
'dc.description.abstract': [
|
||||||
language: null,
|
{
|
||||||
value: 'This is a test community to hold content for the OR2017 demostration'
|
language: null,
|
||||||
},
|
value: 'This is a test community to hold content for the OR2017 demostration'
|
||||||
{
|
}
|
||||||
key: 'dc.description.tableofcontents',
|
],
|
||||||
language: null,
|
'dc.description.tableofcontents': [
|
||||||
value: ''
|
{
|
||||||
},
|
language: null,
|
||||||
{
|
value: ''
|
||||||
key: 'dc.rights',
|
}
|
||||||
language: null,
|
],
|
||||||
value: ''
|
'dc.rights': [
|
||||||
},
|
{
|
||||||
{
|
language: null,
|
||||||
key: 'dc.title',
|
value: ''
|
||||||
language: null,
|
}
|
||||||
value: 'OR2017 - Demonstration'
|
],
|
||||||
}
|
'dc.title': [
|
||||||
]
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'OR2017 - Demonstration'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
Object.assign(new Community(),
|
Object.assign(new Community(),
|
||||||
{
|
{
|
||||||
@@ -160,33 +165,38 @@ export const objects = [
|
|||||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description': [
|
||||||
key: 'dc.description',
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
|
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.description.abstract',
|
'dc.description.abstract': [
|
||||||
language: null,
|
{
|
||||||
value: 'This is a sample top-level community'
|
language: null,
|
||||||
},
|
value: 'This is a sample top-level community'
|
||||||
{
|
}
|
||||||
key: 'dc.description.tableofcontents',
|
],
|
||||||
language: null,
|
'dc.description.tableofcontents': [
|
||||||
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
|
{
|
||||||
},
|
language: null,
|
||||||
{
|
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
|
||||||
key: 'dc.rights',
|
}
|
||||||
language: null,
|
],
|
||||||
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
|
'dc.rights': [
|
||||||
},
|
{
|
||||||
{
|
language: null,
|
||||||
key: 'dc.title',
|
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
|
||||||
language: null,
|
}
|
||||||
value: 'Sample Community'
|
],
|
||||||
}
|
'dc.title': [
|
||||||
]
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Sample Community'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
@@ -60,23 +60,26 @@ describe('AuthResponseParsingService', () => {
|
|||||||
handle: null,
|
handle: null,
|
||||||
id: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
id: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||||
lastActive: '2018-05-14T17:03:31.277+0000',
|
lastActive: '2018-05-14T17:03:31.277+0000',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'eperson.firstname': [
|
||||||
key: 'eperson.firstname',
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: 'User'
|
value: 'User'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'eperson.lastname',
|
'eperson.lastname': [
|
||||||
language: null,
|
{
|
||||||
value: 'Test'
|
language: null,
|
||||||
},
|
value: 'Test'
|
||||||
{
|
}
|
||||||
key: 'eperson.language',
|
],
|
||||||
language: null,
|
'eperson.language': [
|
||||||
value: 'en'
|
{
|
||||||
}
|
language: null,
|
||||||
],
|
value: 'en'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
name: 'User Test',
|
name: 'User Test',
|
||||||
netid: 'myself@testshib.org',
|
netid: 'myself@testshib.org',
|
||||||
requireCertificate: false,
|
requireCertificate: false,
|
||||||
|
17
src/app/core/browse/browse-entry-search-options.model.ts
Normal file
17
src/app/core/browse/browse-entry-search-options.model.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortOptions } from '../cache/models/sort-options.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that defines the search options to be used for fetching browse entries or items
|
||||||
|
* - metadataDefinition: The metadata definition to fetch entries or items for
|
||||||
|
* - pagination: Optional pagination options to use
|
||||||
|
* - sort: Optional sorting options to use
|
||||||
|
* - scope: An optional scope to limit the results within a specific collection or community
|
||||||
|
*/
|
||||||
|
export class BrowseEntrySearchOptions {
|
||||||
|
constructor(public metadataDefinition: string,
|
||||||
|
public pagination?: PaginationComponentOptions,
|
||||||
|
public sort?: SortOptions,
|
||||||
|
public scope?: string) {
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,7 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from
|
|||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
@@ -151,14 +152,14 @@ describe('BrowseService', () => {
|
|||||||
it('should configure a new BrowseEntriesRequest', () => {
|
it('should configure a new BrowseEntriesRequest', () => {
|
||||||
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe());
|
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
service.getBrowseEntriesFor(browseDefinitions[1].id);
|
service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id));
|
||||||
|
|
||||||
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -170,14 +171,14 @@ describe('BrowseService', () => {
|
|||||||
it('should configure a new BrowseItemsRequest', () => {
|
it('should configure a new BrowseItemsRequest', () => {
|
||||||
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
|
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
|
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
|
service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id));
|
||||||
|
|
||||||
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ describe('BrowseService', () => {
|
|||||||
const definitionID = 'invalidID';
|
const definitionID = 'invalidID';
|
||||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||||
|
|
||||||
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
|
expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ describe('BrowseService', () => {
|
|||||||
const definitionID = 'invalidID';
|
const definitionID = 'invalidID';
|
||||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||||
|
|
||||||
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
|
expect(service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -219,44 +220,44 @@ describe('BrowseService', () => {
|
|||||||
}}));
|
}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the URL for the given metadatumKey and linkPath', () => {
|
it('should return the URL for the given metadataKey and linkPath', () => {
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadataKey = 'dc.date.issued';
|
||||||
const linkPath = 'items';
|
const linkPath = 'items';
|
||||||
const expectedURL = browseDefinitions[0]._links[linkPath];
|
const expectedURL = browseDefinitions[0]._links[linkPath];
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work when the definition uses a wildcard in the metadatumKey', () => {
|
it('should work when the definition uses a wildcard in the metadataKey', () => {
|
||||||
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
const metadataKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
||||||
const linkPath = 'items';
|
const linkPath = 'items';
|
||||||
const expectedURL = browseDefinitions[1]._links[linkPath];
|
const expectedURL = browseDefinitions[1]._links[linkPath];
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when the key doesn\'t match', () => {
|
it('should throw an error when the key doesn\'t match', () => {
|
||||||
const metadatumKey = 'dc.title'; // isn't in the definitions
|
const metadataKey = 'dc.title'; // isn't in the definitions
|
||||||
const linkPath = 'items';
|
const linkPath = 'items';
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when the link doesn\'t match', () => {
|
it('should throw an error when the link doesn\'t match', () => {
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadataKey = 'dc.date.issued';
|
||||||
const linkPath = 'collections'; // isn't in the definitions
|
const linkPath = 'collections'; // isn't in the definitions
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -271,10 +272,10 @@ describe('BrowseService', () => {
|
|||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
.returnValue(hot('----'));
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadataKey = 'dc.date.issued';
|
||||||
const linkPath = 'items';
|
const linkPath = 'items';
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||||
const expected = cold('b---', { b: undefined });
|
const expected = cold('b---', { b: undefined });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
@@ -33,13 +33,17 @@ import {
|
|||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that performs all actions that have to do with browse.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
protected linkPath = 'browses';
|
protected linkPath = 'browses';
|
||||||
|
|
||||||
private static toSearchKeyArray(metadatumKey: string): string[] {
|
private static toSearchKeyArray(metadataKey: string): string[] {
|
||||||
const keyParts = metadatumKey.split('.');
|
const keyParts = metadataKey.split('.');
|
||||||
const searchFor = [];
|
const searchFor = [];
|
||||||
searchFor.push('*');
|
searchFor.push('*');
|
||||||
for (let i = 0; i < keyParts.length - 1; i++) {
|
for (let i = 0; i < keyParts.length - 1; i++) {
|
||||||
@@ -47,7 +51,7 @@ export class BrowseService {
|
|||||||
const nextPart = [...prevParts, '*'].join('.');
|
const nextPart = [...prevParts, '*'].join('.');
|
||||||
searchFor.push(nextPart);
|
searchFor.push(nextPart);
|
||||||
}
|
}
|
||||||
searchFor.push(metadatumKey);
|
searchFor.push(metadataKey);
|
||||||
return searchFor;
|
return searchFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,18 +84,18 @@ export class BrowseService {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseEntriesFor(definitionID: string, options: {
|
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
pagination?: PaginationComponentOptions;
|
|
||||||
sort?: SortOptions;
|
|
||||||
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(definitionID),
|
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.entries),
|
map((_links: any) => _links.entries),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
// TODO nearly identical to PaginatedSearchOptions => refactor
|
// TODO nearly identical to PaginatedSearchOptions => refactor
|
||||||
const args = [];
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`scope=${options.scope}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
}
|
}
|
||||||
@@ -133,17 +137,17 @@ export class BrowseService {
|
|||||||
* sort: SortOptions }
|
* sort: SortOptions }
|
||||||
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
||||||
*/
|
*/
|
||||||
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
|
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
pagination?: PaginationComponentOptions;
|
|
||||||
sort?: SortOptions;
|
|
||||||
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
|
|
||||||
const request$ = this.getBrowseDefinitions().pipe(
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(definitionID),
|
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((_links: any) => _links.items),
|
map((_links: any) => _links.items),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const args = [];
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`scope=${options.scope}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(options.sort)) {
|
if (isNotEmpty(options.sort)) {
|
||||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
}
|
}
|
||||||
@@ -179,8 +183,8 @@ export class BrowseService {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
|
||||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
|
||||||
return this.getBrowseDefinitions().pipe(
|
return this.getBrowseDefinitions().pipe(
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
@@ -191,7 +195,7 @@ export class BrowseService {
|
|||||||
),
|
),
|
||||||
map((def: BrowseDefinition) => {
|
map((def: BrowseDefinition) => {
|
||||||
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
||||||
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`);
|
||||||
} else {
|
} else {
|
||||||
return def._links[linkPath];
|
return def._links[linkPath];
|
||||||
}
|
}
|
||||||
|
@@ -8,20 +8,24 @@ import { of as observableOf } from 'rxjs';
|
|||||||
const pageInfo = new PageInfo();
|
const pageInfo = new PageInfo();
|
||||||
const array = [
|
const array = [
|
||||||
Object.assign(new Item(), {
|
Object.assign(new Item(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Item nr 1'
|
value: 'Item nr 1'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
Object.assign(new Item(), {
|
Object.assign(new Item(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Item nr 2'
|
value: 'Item nr 2'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
const paginatedList = new PaginatedList(pageInfo, array);
|
const paginatedList = new PaginatedList(pageInfo, array);
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
|
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
import { MetadataMap } from '../../shared/metadata.interfaces';
|
||||||
import { Metadatum } from '../../shared/metadatum.model';
|
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { mapsTo } from '../builders/build-decorators';
|
import { mapsTo } from '../builders/build-decorators';
|
||||||
import { NormalizedObject } from './normalized-object.model';
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
@@ -46,10 +45,10 @@ export class NormalizedDSpaceObject<T extends DSpaceObject> extends NormalizedOb
|
|||||||
type: ResourceType;
|
type: ResourceType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* All metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserializeAs(Metadatum)
|
@autoserialize
|
||||||
metadata: Metadatum[];
|
metadata: MetadataMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
|
15
src/app/core/cache/response.models.ts
vendored
15
src/app/core/cache/response.models.ts
vendored
@@ -33,6 +33,9 @@ export class DSOSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse
|
||||||
|
*/
|
||||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
||||||
@@ -43,6 +46,9 @@ export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
|
||||||
|
*/
|
||||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
||||||
@@ -53,6 +59,9 @@ export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
|
||||||
|
*/
|
||||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
||||||
@@ -63,6 +72,9 @@ export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing exactly one MetadataSchema
|
||||||
|
*/
|
||||||
export class MetadataschemaSuccessResponse extends RestResponse {
|
export class MetadataschemaSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public metadataschema: MetadataSchema,
|
public metadataschema: MetadataSchema,
|
||||||
@@ -72,6 +84,9 @@ export class MetadataschemaSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing exactly one MetadataField
|
||||||
|
*/
|
||||||
export class MetadatafieldSuccessResponse extends RestResponse {
|
export class MetadatafieldSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public metadatafield: MetadataField,
|
public metadatafield: MetadataField,
|
||||||
|
@@ -105,21 +105,6 @@ describe('BrowseEntriesResponseParsingService', () => {
|
|||||||
} as DSpaceRESTV2Response;
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
const invalidResponseNotAList = {
|
const invalidResponseNotAList = {
|
||||||
payload: {
|
|
||||||
authority: null,
|
|
||||||
value: 'Arulmozhiyal, Ramaswamy',
|
|
||||||
valueLang: null,
|
|
||||||
count: 1,
|
|
||||||
type: 'browseEntry',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: 'https://rest.api/discover/browses/author/entries'
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
statusCode: '200'
|
statusCode: '200'
|
||||||
} as DSpaceRESTV2Response;
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
@@ -30,10 +30,12 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
if (isNotEmpty(data.payload)) {
|
||||||
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
let browseEntries = [];
|
||||||
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
||||||
|
browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
}
|
||||||
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
|
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
|
@@ -24,13 +24,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
|||||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||||
handle: '10986/17472',
|
handle: '10986/17472',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.creator': [
|
||||||
key: 'dc.creator',
|
{
|
||||||
value: 'World Bank',
|
value: 'World Bank',
|
||||||
language: null
|
language: null
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
},
|
||||||
inArchive: true,
|
inArchive: true,
|
||||||
discoverable: true,
|
discoverable: true,
|
||||||
withdrawn: false,
|
withdrawn: false,
|
||||||
@@ -56,13 +57,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
|||||||
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
|
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
|
||||||
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
|
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
|
||||||
handle: '10986/17475',
|
handle: '10986/17475',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.creator': [
|
||||||
key: 'dc.creator',
|
{
|
||||||
value: 'World Bank',
|
value: 'World Bank',
|
||||||
language: null
|
language: null
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
},
|
||||||
inArchive: true,
|
inArchive: true,
|
||||||
discoverable: true,
|
discoverable: true,
|
||||||
withdrawn: false,
|
withdrawn: false,
|
||||||
@@ -115,13 +117,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
|||||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||||
handle: '10986/17472',
|
handle: '10986/17472',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.creator': [
|
||||||
key: 'dc.creator',
|
{
|
||||||
value: 'World Bank',
|
value: 'World Bank',
|
||||||
language: null
|
language: null
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
},
|
||||||
inArchive: true,
|
inArchive: true,
|
||||||
discoverable: true,
|
discoverable: true,
|
||||||
withdrawn: false,
|
withdrawn: false,
|
||||||
|
@@ -29,7 +29,13 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const processRequestDTO = this.process<NormalizedObject<DSpaceObject>, ResourceType>(data.payload, request.uuid);
|
let processRequestDTO;
|
||||||
|
// Prevent empty pages returning an error, initialize empty array instead.
|
||||||
|
if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) {
|
||||||
|
processRequestDTO = { page: [] };
|
||||||
|
} else {
|
||||||
|
processRequestDTO = this.process<NormalizedObject<DSpaceObject>, ResourceType>(data.payload, request.uuid);
|
||||||
|
}
|
||||||
let objectList = processRequestDTO;
|
let objectList = processRequestDTO;
|
||||||
|
|
||||||
if (hasNoValue(processRequestDTO)) {
|
if (hasNoValue(processRequestDTO)) {
|
||||||
|
@@ -15,9 +15,9 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
import { FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
|
||||||
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
|
|
||||||
|
@@ -16,6 +16,9 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataSchemaDataService extends DataService<MetadataSchema> {
|
export class MetadataSchemaDataService extends DataService<MetadataSchema> {
|
||||||
protected linkPath = 'metadataschemas';
|
protected linkPath = 'metadataschemas';
|
||||||
|
@@ -6,6 +6,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
import { MetadataField } from '../metadata/metadatafield.model';
|
import { MetadataField } from '../metadata/metadatafield.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadatafieldParsingService implements ResponseParsingService {
|
export class MetadatafieldParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
@@ -220,6 +220,9 @@ export class IntegrationRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to create a MetadataSchema
|
||||||
|
*/
|
||||||
export class CreateMetadataSchemaRequest extends PostRequest {
|
export class CreateMetadataSchemaRequest extends PostRequest {
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
super(uuid, href, body, options);
|
super(uuid, href, body, options);
|
||||||
@@ -230,6 +233,9 @@ export class CreateMetadataSchemaRequest extends PostRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to update a MetadataSchema
|
||||||
|
*/
|
||||||
export class UpdateMetadataSchemaRequest extends PutRequest {
|
export class UpdateMetadataSchemaRequest extends PutRequest {
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
super(uuid, href, body, options);
|
super(uuid, href, body, options);
|
||||||
@@ -240,6 +246,9 @@ export class UpdateMetadataSchemaRequest extends PutRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to create a MetadataField
|
||||||
|
*/
|
||||||
export class CreateMetadataFieldRequest extends PostRequest {
|
export class CreateMetadataFieldRequest extends PostRequest {
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
super(uuid, href, body, options);
|
super(uuid, href, body, options);
|
||||||
@@ -250,6 +259,9 @@ export class CreateMetadataFieldRequest extends PostRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to update a MetadataField
|
||||||
|
*/
|
||||||
export class UpdateMetadataFieldRequest extends PutRequest {
|
export class UpdateMetadataFieldRequest extends PutRequest {
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
super(uuid, href, body, options);
|
super(uuid, href, body, options);
|
||||||
|
@@ -110,6 +110,11 @@ function resetResponseTimestamps(state: RequestState, action: ResetResponseTimes
|
|||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a request from the RequestState
|
||||||
|
* @param state The current RequestState
|
||||||
|
* @param action The RequestRemoveAction to perform
|
||||||
|
*/
|
||||||
function removeRequest(state: RequestState, action: RequestRemoveAction): RequestState {
|
function removeRequest(state: RequestState, action: RequestRemoveAction): RequestState {
|
||||||
const newState = Object.create(null);
|
const newState = Object.create(null);
|
||||||
for (const value in state) {
|
for (const value in state) {
|
||||||
|
@@ -55,10 +55,23 @@ export class RequestService {
|
|||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
|
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
||||||
|
* contains a given substring
|
||||||
|
* @param selector MemoizedSelector to start from
|
||||||
|
* @param name The name of the index substate we're fetching request UUIDs from
|
||||||
|
* @param href Substring that the request's href should contain
|
||||||
|
*/
|
||||||
private uuidsFromHrefSubstringSelector(selector: MemoizedSelector<any, IndexState>, name: string, href: string): MemoizedSelector<any, string[]> {
|
private uuidsFromHrefSubstringSelector(selector: MemoizedSelector<any, IndexState>, name: string, href: string): MemoizedSelector<any, string[]> {
|
||||||
return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
|
return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
|
||||||
|
* @param state The IndexState
|
||||||
|
* @param name The name of the index substate we're fetching request UUIDs from
|
||||||
|
* @param href Substring that the request's href should contain
|
||||||
|
*/
|
||||||
private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
|
private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
if (isNotEmpty(state)) {
|
if (isNotEmpty(state)) {
|
||||||
|
@@ -7,7 +7,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
import { Metadatum } from '../shared/metadatum.model';
|
import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchResponseParsingService implements ResponseParsingService {
|
export class SearchResponseParsingService implements ResponseParsingService {
|
||||||
@@ -16,17 +16,17 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
|||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const payload = data.payload._embedded.searchResult;
|
const payload = data.payload._embedded.searchResult;
|
||||||
const hitHighlights = payload._embedded.objects
|
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||||
.map((object) => object.hitHighlights)
|
.map((object) => object.hitHighlights)
|
||||||
.map((hhObject) => {
|
.map((hhObject) => {
|
||||||
|
const mdMap: MetadataMap = {};
|
||||||
if (hhObject) {
|
if (hhObject) {
|
||||||
return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), {
|
for (const key of Object.keys(hhObject)) {
|
||||||
key: key,
|
const value: MetadataValue = { value: hhObject[key].join('...'), language: null };
|
||||||
value: hhObject[key].join('...')
|
mdMap[key] = [ value ];
|
||||||
}))
|
}
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
return mdMap;
|
||||||
});
|
});
|
||||||
|
|
||||||
const dsoSelfLinks = payload._embedded.objects
|
const dsoSelfLinks = payload._embedded.objects
|
||||||
|
@@ -91,9 +91,9 @@ export class DSpaceRESTv2Service {
|
|||||||
const form: FormData = new FormData();
|
const form: FormData = new FormData();
|
||||||
form.append('name', dso.name);
|
form.append('name', dso.name);
|
||||||
if (dso.metadata) {
|
if (dso.metadata) {
|
||||||
for (const i of Object.keys(dso.metadata)) {
|
for (const key of Object.keys(dso.metadata)) {
|
||||||
if (isNotEmpty(dso.metadata[i].value)) {
|
for (const value of dso.allMetadataValues(key)) {
|
||||||
form.append(dso.metadata[i].key, dso.metadata[i].value);
|
form.append(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,11 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove values from the IndexState's substate that contain a given substring
|
||||||
|
* @param state The IndexState to remove values from
|
||||||
|
* @param action The RemoveFromIndexByValueAction containing the necessary information to remove the values
|
||||||
|
*/
|
||||||
function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
|
function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
|
||||||
const subState = state[action.payload.name];
|
const subState = state[action.payload.name];
|
||||||
const newSubState = Object.create(null);
|
const newSubState = Object.create(null);
|
||||||
|
@@ -37,6 +37,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { EmptyError } from 'rxjs/internal-compatibility';
|
import { EmptyError } from 'rxjs/internal-compatibility';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
|
import { MetadataValue } from '../shared/metadata.interfaces';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -152,7 +153,7 @@ describe('MetadataService', () => {
|
|||||||
expect(title.getTitle()).toEqual('Test PowerPoint Document');
|
expect(title.getTitle()).toEqual('Test PowerPoint Document');
|
||||||
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
|
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
|
||||||
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
|
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
|
||||||
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26T19:58:25Z');
|
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26');
|
||||||
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
|
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
|
||||||
expect(tagStore.get('citation_language')[0].content).toEqual('en');
|
expect(tagStore.get('citation_language')[0].content).toEqual('en');
|
||||||
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
|
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
|
||||||
@@ -216,23 +217,18 @@ describe('MetadataService', () => {
|
|||||||
|
|
||||||
const mockType = (mockItem: Item, type: string): Item => {
|
const mockType = (mockItem: Item, type: string): Item => {
|
||||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||||
for (const metadatum of typedMockItem.metadata) {
|
typedMockItem.metadata['dc.type'] = [ { value: type } ] as MetadataValue[];
|
||||||
if (metadatum.key === 'dc.type') {
|
|
||||||
metadatum.value = type;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return typedMockItem;
|
return typedMockItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockPublisher = (mockItem: Item): Item => {
|
const mockPublisher = (mockItem: Item): Item => {
|
||||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||||
publishedMockItem.metadata.push({
|
publishedMockItem.metadata['dc.publisher'] = [
|
||||||
uuid: 'b3826cf5-5f07-44cf-88d8-2da968354d18',
|
{
|
||||||
key: 'dc.publisher',
|
language: 'en_US',
|
||||||
language: 'en_US',
|
value: 'Mock Publisher'
|
||||||
value: 'Mock Publisher'
|
}
|
||||||
});
|
] as MetadataValue[];
|
||||||
return publishedMockItem;
|
return publishedMockItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -294,6 +294,10 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasType(value: string): boolean {
|
||||||
|
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this._item is a dissertation
|
* Returns true if this._item is a dissertation
|
||||||
*
|
*
|
||||||
@@ -301,14 +305,7 @@ export class MetadataService {
|
|||||||
* true if this._item has a dc.type equal to 'Thesis'
|
* true if this._item has a dc.type equal to 'Thesis'
|
||||||
*/
|
*/
|
||||||
private isDissertation(): boolean {
|
private isDissertation(): boolean {
|
||||||
let isDissertation = false;
|
return this.hasType('thesis');
|
||||||
for (const metadatum of this.currentObject.value.metadata) {
|
|
||||||
if (metadatum.key === 'dc.type') {
|
|
||||||
isDissertation = metadatum.value.toLowerCase() === 'thesis';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isDissertation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,40 +315,15 @@ export class MetadataService {
|
|||||||
* true if this._item has a dc.type equal to 'Technical Report'
|
* true if this._item has a dc.type equal to 'Technical Report'
|
||||||
*/
|
*/
|
||||||
private isTechReport(): boolean {
|
private isTechReport(): boolean {
|
||||||
let isTechReport = false;
|
return this.hasType('technical report');
|
||||||
for (const metadatum of this.currentObject.value.metadata) {
|
|
||||||
if (metadatum.key === 'dc.type') {
|
|
||||||
isTechReport = metadatum.value.toLowerCase() === 'technical report';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isTechReport;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMetaTagValue(key: string): string {
|
private getMetaTagValue(key: string): string {
|
||||||
let value: string;
|
return this.currentObject.value.firstMetadataValue(key);
|
||||||
for (const metadatum of this.currentObject.value.metadata) {
|
|
||||||
if (metadatum.key === key) {
|
|
||||||
value = metadatum.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFirstMetaTagValue(keys: string[]): string {
|
private getFirstMetaTagValue(keys: string[]): string {
|
||||||
let value: string;
|
return this.currentObject.value.firstMetadataValue(keys);
|
||||||
for (const metadatum of this.currentObject.value.metadata) {
|
|
||||||
for (const key of keys) {
|
|
||||||
if (key === metadatum.key) {
|
|
||||||
value = metadatum.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value !== undefined) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMetaTagValuesAndCombine(key: string): string {
|
private getMetaTagValuesAndCombine(key: string): string {
|
||||||
@@ -359,15 +331,7 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getMetaTagValues(keys: string[]): string[] {
|
private getMetaTagValues(keys: string[]): string[] {
|
||||||
const values: string[] = [];
|
return this.currentObject.value.allMetadataValues(keys);
|
||||||
for (const metadatum of this.currentObject.value.metadata) {
|
|
||||||
for (const key of keys) {
|
|
||||||
if (key === metadatum.key) {
|
|
||||||
values.push(metadatum.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addMetaTag(property: string, content: string): void {
|
private addMetaTag(property: string, content: string): void {
|
||||||
|
@@ -1,21 +1,35 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize } from 'cerialize';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { mapsTo } from '../cache/builders/build-decorators';
|
import { mapsTo } from '../cache/builders/build-decorators';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
import { MetadataSchema } from './metadataschema.model';
|
import { MetadataSchema } from './metadataschema.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized class for a DSpace MetadataSchema
|
||||||
|
*/
|
||||||
@mapsTo(MetadataSchema)
|
@mapsTo(MetadataSchema)
|
||||||
export class NormalizedMetadataSchema extends NormalizedObject<MetadataSchema> implements ListableObject {
|
export class NormalizedMetadataSchema extends NormalizedObject<MetadataSchema> implements ListableObject {
|
||||||
|
/**
|
||||||
|
* The unique identifier for this schema
|
||||||
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The REST link to itself
|
||||||
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
self: string;
|
self: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique prefix that defines this schema
|
||||||
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace for this schema
|
||||||
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
namespace: string;
|
namespace: string;
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,11 @@ import { MetadataSchema } from '../metadata/metadataschema.model';
|
|||||||
import { MetadataField } from '../metadata/metadatafield.model';
|
import { MetadataField } from '../metadata/metadatafield.model';
|
||||||
import { BitstreamFormat } from './mock-bitstream-format.model';
|
import { BitstreamFormat } from './mock-bitstream-format.model';
|
||||||
import {
|
import {
|
||||||
|
CreateMetadataFieldRequest,
|
||||||
CreateMetadataSchemaRequest,
|
CreateMetadataSchemaRequest,
|
||||||
DeleteRequest,
|
DeleteRequest,
|
||||||
GetRequest,
|
GetRequest,
|
||||||
RestRequest,
|
RestRequest, UpdateMetadataFieldRequest,
|
||||||
UpdateMetadataSchemaRequest
|
UpdateMetadataSchemaRequest
|
||||||
} from '../data/request.models';
|
} from '../data/request.models';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
@@ -375,6 +376,14 @@ export class RegistryService {
|
|||||||
return this.store.pipe(select(selectedMetadataFieldsSelector));
|
return this.store.pipe(select(selectedMetadataFieldsSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataSchema
|
||||||
|
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateMetadataSchemaRequest is used
|
||||||
|
* - On update, a UpdateMetadataSchemaRequest is used
|
||||||
|
* @param schema The MetadataSchema to create or update
|
||||||
|
*/
|
||||||
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
||||||
const isUpdate = hasValue(schema.id);
|
const isUpdate = hasValue(schema.id);
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
@@ -438,6 +447,14 @@ export class RegistryService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataField
|
||||||
|
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateMetadataFieldRequest is used
|
||||||
|
* - On update, a UpdateMetadataFieldRequest is used
|
||||||
|
* @param field The MetadataField to create or update
|
||||||
|
*/
|
||||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||||
const isUpdate = hasValue(field.id);
|
const isUpdate = hasValue(field.id);
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
@@ -455,9 +472,9 @@ export class RegistryService {
|
|||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'application/json');
|
headers = headers.append('Content-Type', 'application/json');
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(field), options);
|
return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options);
|
||||||
} else {
|
} else {
|
||||||
return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(field));
|
return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -16,7 +16,7 @@ export class Collection extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description
|
* Corresponds to the metadata field dc.description
|
||||||
*/
|
*/
|
||||||
get introductoryText(): string {
|
get introductoryText(): string {
|
||||||
return this.findMetadata('dc.description');
|
return this.firstMetadataValue('dc.description');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +24,7 @@ export class Collection extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description.abstract
|
* Corresponds to the metadata field dc.description.abstract
|
||||||
*/
|
*/
|
||||||
get shortDescription(): string {
|
get shortDescription(): string {
|
||||||
return this.findMetadata('dc.description.abstract');
|
return this.firstMetadataValue('dc.description.abstract');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +32,7 @@ export class Collection extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.rights
|
* Corresponds to the metadata field dc.rights
|
||||||
*/
|
*/
|
||||||
get copyrightText(): string {
|
get copyrightText(): string {
|
||||||
return this.findMetadata('dc.rights');
|
return this.firstMetadataValue('dc.rights');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +40,7 @@ export class Collection extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.rights.license
|
* Corresponds to the metadata field dc.rights.license
|
||||||
*/
|
*/
|
||||||
get license(): string {
|
get license(): string {
|
||||||
return this.findMetadata('dc.rights.license');
|
return this.firstMetadataValue('dc.rights.license');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +48,7 @@ export class Collection extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description.tableofcontents
|
* Corresponds to the metadata field dc.description.tableofcontents
|
||||||
*/
|
*/
|
||||||
get sidebarText(): string {
|
get sidebarText(): string {
|
||||||
return this.findMetadata('dc.description.tableofcontents');
|
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,7 +17,7 @@ export class Community extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description
|
* Corresponds to the metadata field dc.description
|
||||||
*/
|
*/
|
||||||
get introductoryText(): string {
|
get introductoryText(): string {
|
||||||
return this.findMetadata('dc.description');
|
return this.firstMetadataValue('dc.description');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ export class Community extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description.abstract
|
* Corresponds to the metadata field dc.description.abstract
|
||||||
*/
|
*/
|
||||||
get shortDescription(): string {
|
get shortDescription(): string {
|
||||||
return this.findMetadata('dc.description.abstract');
|
return this.firstMetadataValue('dc.description.abstract');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +33,7 @@ export class Community extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.rights
|
* Corresponds to the metadata field dc.rights
|
||||||
*/
|
*/
|
||||||
get copyrightText(): string {
|
get copyrightText(): string {
|
||||||
return this.findMetadata('dc.rights');
|
return this.firstMetadataValue('dc.rights');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,7 @@ export class Community extends DSpaceObject {
|
|||||||
* Corresponds to the metadata field dc.description.tableofcontents
|
* Corresponds to the metadata field dc.description.tableofcontents
|
||||||
*/
|
*/
|
||||||
get sidebarText(): string {
|
get sidebarText(): string {
|
||||||
return this.findMetadata('dc.description.tableofcontents');
|
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Metadatum } from './metadatum.model'
|
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { Metadata } from './metadata.model';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
@@ -35,14 +35,14 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
* The name for this DSpaceObject
|
* The name for this DSpaceObject
|
||||||
*/
|
*/
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.findMetadata('dc.title');
|
return this.firstMetadataValue('dc.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* All metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
metadata: Metadatum[] = [];
|
metadata: MetadataMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
@@ -55,41 +55,58 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
owner: Observable<RemoteData<DSpaceObject>>;
|
owner: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a metadata field by key and language
|
* Gets all matching metadata in this DSpaceObject.
|
||||||
*
|
*
|
||||||
* This method returns the value of the first element
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
* in the metadata array that matches the provided
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
* key and language
|
* @returns {MetadataValue[]} the matching values or an empty array.
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* @param language
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
findMetadata(key: string, language?: string): string {
|
allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] {
|
||||||
const metadatum = this.metadata.find((m: Metadatum) => {
|
return Metadata.all(this.metadata, keyOrKeys, valueFilter);
|
||||||
return m.key === key && (isEmpty(language) || m.language === language)
|
|
||||||
});
|
|
||||||
if (isNotEmpty(metadatum)) {
|
|
||||||
return metadatum.value;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find metadata by an array of keys
|
* Like [[allMetadata]], but only returns string values.
|
||||||
*
|
*
|
||||||
* This method returns the values of the element
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
* in the metadata array that match the provided
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
* key(s)
|
* @returns {string[]} the matching string values or an empty array.
|
||||||
*
|
|
||||||
* @param key(s)
|
|
||||||
* @return Array<Metadatum>
|
|
||||||
*/
|
*/
|
||||||
filterMetadata(keys: string[]): Metadatum[] {
|
allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] {
|
||||||
return this.metadata.filter((metadatum: Metadatum) => {
|
return Metadata.allValues(this.metadata, keyOrKeys, valueFilter);
|
||||||
return keys.some((key) => key === metadatum.key);
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Gets the first matching MetadataValue object in this DSpaceObject, or `undefined`.
|
||||||
|
*
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {MetadataValue} the first matching value, or `undefined`.
|
||||||
|
*/
|
||||||
|
firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue {
|
||||||
|
return Metadata.first(this.metadata, keyOrKeys, valueFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [[firstMetadata]], but only returns a string value, or `undefined`.
|
||||||
|
*
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {string} the first matching string value, or `undefined`.
|
||||||
|
*/
|
||||||
|
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||||
|
return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a matching metadata value in this DSpaceObject.
|
||||||
|
*
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {boolean} whether a match is found.
|
||||||
|
*/
|
||||||
|
hasMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): boolean {
|
||||||
|
return Metadata.has(this.metadata, keyOrKeys, valueFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
30
src/app/core/shared/metadata.interfaces.ts
Normal file
30
src/app/core/shared/metadata.interfaces.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** A map of metadata keys to an ordered list of MetadataValue objects. */
|
||||||
|
export interface MetadataMap {
|
||||||
|
[ key: string ]: MetadataValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A single metadata value and its properties. */
|
||||||
|
export interface MetadataValue {
|
||||||
|
|
||||||
|
/** The language. */
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
/** The string value. */
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constraints for matching metadata values. */
|
||||||
|
export interface MetadataValueFilter {
|
||||||
|
|
||||||
|
/** The language constraint. */
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
/** The value constraint. */
|
||||||
|
value?: string;
|
||||||
|
|
||||||
|
/** Whether the value constraint should match without regard to case. */
|
||||||
|
ignoreCase?: boolean;
|
||||||
|
|
||||||
|
/** Whether the value constraint should match as a substring. */
|
||||||
|
substring?: boolean;
|
||||||
|
}
|
175
src/app/core/shared/metadata.model.spec.ts
Normal file
175
src/app/core/shared/metadata.model.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { isUndefined } from '../../shared/empty.util';
|
||||||
|
import { MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||||
|
import { Metadata } from './metadata.model';
|
||||||
|
|
||||||
|
const mdValue = (value: string, language?: string): MetadataValue => {
|
||||||
|
return { value: value, language: isUndefined(language) ? null : language };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcDescription = mdValue('Some description');
|
||||||
|
const dcAbstract = mdValue('Some abstract');
|
||||||
|
const dcTitle0 = mdValue('Title 0');
|
||||||
|
const dcTitle1 = mdValue('Title 1');
|
||||||
|
const dcTitle2 = mdValue('Title 2', 'en_US');
|
||||||
|
const bar = mdValue('Bar');
|
||||||
|
|
||||||
|
const singleMap = { 'dc.title': [ dcTitle0 ] };
|
||||||
|
|
||||||
|
const multiMap = {
|
||||||
|
'dc.description': [ dcDescription ],
|
||||||
|
'dc.description.abstract': [ dcAbstract ],
|
||||||
|
'dc.title': [ dcTitle1, dcTitle2 ],
|
||||||
|
'foo': [ bar ]
|
||||||
|
};
|
||||||
|
|
||||||
|
const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => {
|
||||||
|
const keys = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ];
|
||||||
|
describe('and key' + (keys.length === 1 ? (' ' + keys[0]) : ('s ' + JSON.stringify(keys)))
|
||||||
|
+ ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => {
|
||||||
|
const result = fn(mapOrMaps, keys, filter);
|
||||||
|
let shouldReturn;
|
||||||
|
if (resultKind === 'boolean') {
|
||||||
|
shouldReturn = expected;
|
||||||
|
} else if (isUndefined(expected)) {
|
||||||
|
shouldReturn = 'undefined';
|
||||||
|
} else if (expected instanceof Array) {
|
||||||
|
shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '')
|
||||||
|
+ resultKind + (expected.length !== 1 ? 's' : '');
|
||||||
|
} else {
|
||||||
|
shouldReturn = 'a ' + resultKind;
|
||||||
|
}
|
||||||
|
it('should return ' + shouldReturn, () => {
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Metadata', () => {
|
||||||
|
|
||||||
|
describe('all method', () => {
|
||||||
|
|
||||||
|
const testAll = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
|
||||||
|
testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, filter);
|
||||||
|
|
||||||
|
describe('with emptyMap', () => {
|
||||||
|
testAll({}, 'foo', []);
|
||||||
|
testAll({}, '*', []);
|
||||||
|
});
|
||||||
|
describe('with singleMap', () => {
|
||||||
|
testAll(singleMap, 'foo', []);
|
||||||
|
testAll(singleMap, '*', [ dcTitle0 ]);
|
||||||
|
testAll(singleMap, '*', [], { value: 'baz' });
|
||||||
|
testAll(singleMap, 'dc.title', [ dcTitle0 ]);
|
||||||
|
testAll(singleMap, 'dc.*', [ dcTitle0 ]);
|
||||||
|
});
|
||||||
|
describe('with multiMap', () => {
|
||||||
|
testAll(multiMap, 'foo', [ bar ]);
|
||||||
|
testAll(multiMap, '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]);
|
||||||
|
testAll(multiMap, 'dc.title', [ dcTitle1, dcTitle2 ]);
|
||||||
|
testAll(multiMap, 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]);
|
||||||
|
testAll(multiMap, [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]);
|
||||||
|
});
|
||||||
|
describe('with [ singleMap, multiMap ]', () => {
|
||||||
|
testAll([ singleMap, multiMap ], 'foo', [ bar ]);
|
||||||
|
testAll([ singleMap, multiMap ], '*', [ dcTitle0 ]);
|
||||||
|
testAll([ singleMap, multiMap ], 'dc.title', [ dcTitle0 ]);
|
||||||
|
testAll([ singleMap, multiMap ], 'dc.*', [ dcTitle0 ]);
|
||||||
|
});
|
||||||
|
describe('with [ multiMap, singleMap ]', () => {
|
||||||
|
testAll([ multiMap, singleMap ], 'foo', [ bar ]);
|
||||||
|
testAll([ multiMap, singleMap ], '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]);
|
||||||
|
testAll([ multiMap, singleMap ], 'dc.title', [ dcTitle1, dcTitle2 ]);
|
||||||
|
testAll([ multiMap, singleMap ], 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]);
|
||||||
|
testAll([ multiMap, singleMap ], [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allValues method', () => {
|
||||||
|
|
||||||
|
const testAllValues = (mapOrMaps, keyOrKeys, expected) =>
|
||||||
|
testMethod(Metadata.allValues, 'string', mapOrMaps, keyOrKeys, expected);
|
||||||
|
|
||||||
|
describe('with emptyMap', () => {
|
||||||
|
testAllValues({}, '*', []);
|
||||||
|
});
|
||||||
|
describe('with singleMap', () => {
|
||||||
|
testAllValues([ singleMap, multiMap ], '*', [ dcTitle0.value ]);
|
||||||
|
});
|
||||||
|
describe('with [ multiMap, singleMap ]', () => {
|
||||||
|
testAllValues([ multiMap, singleMap ], '*', [ dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('first method', () => {
|
||||||
|
|
||||||
|
const testFirst = (mapOrMaps, keyOrKeys, expected) =>
|
||||||
|
testMethod(Metadata.first, 'value', mapOrMaps, keyOrKeys, expected);
|
||||||
|
|
||||||
|
describe('with emptyMap', () => {
|
||||||
|
testFirst({}, '*', undefined);
|
||||||
|
});
|
||||||
|
describe('with singleMap', () => {
|
||||||
|
testFirst(singleMap, '*', dcTitle0);
|
||||||
|
});
|
||||||
|
describe('with [ multiMap, singleMap ]', () => {
|
||||||
|
testFirst([ multiMap, singleMap ], '*', dcDescription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('firstValue method', () => {
|
||||||
|
|
||||||
|
const testFirstValue = (mapOrMaps, keyOrKeys, expected) =>
|
||||||
|
testMethod(Metadata.firstValue, 'value', mapOrMaps, keyOrKeys, expected);
|
||||||
|
|
||||||
|
describe('with emptyMap', () => {
|
||||||
|
testFirstValue({}, '*', undefined);
|
||||||
|
});
|
||||||
|
describe('with singleMap', () => {
|
||||||
|
testFirstValue(singleMap, '*', dcTitle0.value);
|
||||||
|
});
|
||||||
|
describe('with [ multiMap, singleMap ]', () => {
|
||||||
|
testFirstValue([ multiMap, singleMap ], '*', dcDescription.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('has method', () => {
|
||||||
|
|
||||||
|
const testHas = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
|
||||||
|
testMethod(Metadata.has, 'boolean', mapOrMaps, keyOrKeys, expected, filter);
|
||||||
|
|
||||||
|
describe('with emptyMap', () => {
|
||||||
|
testHas({}, '*', false);
|
||||||
|
});
|
||||||
|
describe('with singleMap', () => {
|
||||||
|
testHas(singleMap, '*', true);
|
||||||
|
testHas(singleMap, '*', false, { value: 'baz' });
|
||||||
|
});
|
||||||
|
describe('with [ multiMap, singleMap ]', () => {
|
||||||
|
testHas([ multiMap, singleMap ], '*', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('valueMatches method', () => {
|
||||||
|
|
||||||
|
const testValueMatches = (value: MetadataValue, expected: boolean, filter?: MetadataValueFilter) => {
|
||||||
|
describe('with value ' + JSON.stringify(value) + ' and filter '
|
||||||
|
+ (isUndefined(filter) ? 'undefined' : JSON.stringify(filter)), () => {
|
||||||
|
const result = Metadata.valueMatches(value, filter);
|
||||||
|
it('should return ' + expected, () => {
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
testValueMatches(mdValue('a'), true);
|
||||||
|
testValueMatches(mdValue('a'), true, { value: 'a' });
|
||||||
|
testValueMatches(mdValue('a'), false, { value: 'A' });
|
||||||
|
testValueMatches(mdValue('a'), true, { value: 'A', ignoreCase: true });
|
||||||
|
testValueMatches(mdValue('ab'), false, { value: 'b' });
|
||||||
|
testValueMatches(mdValue('ab'), true, { value: 'b', substring: true });
|
||||||
|
testValueMatches(mdValue('a'), true, { language: null });
|
||||||
|
testValueMatches(mdValue('a'), false, { language: 'en_US' });
|
||||||
|
testValueMatches(mdValue('a', 'en_US'), true, { language: 'en_US' });
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
163
src/app/core/shared/metadata.model.ts
Normal file
163
src/app/core/shared/metadata.model.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
|
||||||
|
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for working with DSpace object metadata.
|
||||||
|
*
|
||||||
|
* When specifying metadata keys, wildcards are supported, so `'*'` will match all keys, `'dc.date.*'` will
|
||||||
|
* match all qualified dc dates, and so on. Exact keys will be evaluated (and matches returned) in the order
|
||||||
|
* they are given.
|
||||||
|
*
|
||||||
|
* When multiple keys in a map match a given wildcard, they are evaluated in the order they are stored in
|
||||||
|
* the map (alphanumeric if obtained from the REST api). If duplicate or overlapping keys are specified, the
|
||||||
|
* first one takes precedence. For example, specifying `['dc.date', 'dc.*', '*']` will cause any `dc.date`
|
||||||
|
* values to be evaluated (and returned, if matched) first, followed by any other `dc` metadata values,
|
||||||
|
* followed by any other (non-dc) metadata values.
|
||||||
|
*/
|
||||||
|
export class Metadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all matching metadata in the map(s).
|
||||||
|
*
|
||||||
|
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
|
||||||
|
* checked in order, and only values from the first with at least one match will be returned.
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {MetadataValue[]} the matching values or an empty array.
|
||||||
|
*/
|
||||||
|
public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||||
|
filter?: MetadataValueFilter): MetadataValue[] {
|
||||||
|
const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [ mapOrMaps ];
|
||||||
|
const matches: MetadataValue[] = [];
|
||||||
|
for (const mdMap of mdMaps) {
|
||||||
|
for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) {
|
||||||
|
const candidates = mdMap[mdKey];
|
||||||
|
if (candidates) {
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (Metadata.valueMatches(candidate, filter)) {
|
||||||
|
matches.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isEmpty(matches)) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [[Metadata.all]], but only returns string values.
|
||||||
|
*
|
||||||
|
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
|
||||||
|
* checked in order, and only values from the first with at least one match will be returned.
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {string[]} the matching string values or an empty array.
|
||||||
|
*/
|
||||||
|
public static allValues(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||||
|
filter?: MetadataValueFilter): string[] {
|
||||||
|
return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first matching MetadataValue object in the map(s), or `undefined`.
|
||||||
|
*
|
||||||
|
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {MetadataValue} the first matching value, or `undefined`.
|
||||||
|
*/
|
||||||
|
public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||||
|
filter?: MetadataValueFilter): MetadataValue {
|
||||||
|
const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [ mdMapOrMaps ];
|
||||||
|
for (const mdMap of mdMaps) {
|
||||||
|
for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) {
|
||||||
|
const values: MetadataValue[] = mdMap[key];
|
||||||
|
if (values) {
|
||||||
|
return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [[Metadata.first]], but only returns a string value, or `undefined`.
|
||||||
|
*
|
||||||
|
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {string} the first matching string value, or `undefined`.
|
||||||
|
*/
|
||||||
|
public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||||
|
filter?: MetadataValueFilter): string {
|
||||||
|
const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter);
|
||||||
|
return isUndefined(value) ? undefined : value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a matching metadata value in the given map(s).
|
||||||
|
*
|
||||||
|
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
|
* @returns {boolean} whether a match is found.
|
||||||
|
*/
|
||||||
|
public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||||
|
filter?: MetadataValueFilter): boolean {
|
||||||
|
return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a value matches a filter.
|
||||||
|
*
|
||||||
|
* @param {MetadataValue} mdValue the value to check.
|
||||||
|
* @param {MetadataValueFilter} filter the filter to use.
|
||||||
|
* @returns {boolean} whether the filter matches, or true if no filter is given.
|
||||||
|
*/
|
||||||
|
public static valueMatches(mdValue: MetadataValue, filter: MetadataValueFilter) {
|
||||||
|
if (!filter) {
|
||||||
|
return true;
|
||||||
|
} else if (filter.language && filter.language !== mdValue.language) {
|
||||||
|
return false;
|
||||||
|
} else if (filter.value) {
|
||||||
|
let fValue = filter.value;
|
||||||
|
let mValue = mdValue.value;
|
||||||
|
if (filter.ignoreCase) {
|
||||||
|
fValue = filter.value.toLowerCase();
|
||||||
|
mValue = mdValue.value.toLowerCase();
|
||||||
|
}
|
||||||
|
if (filter.substring) {
|
||||||
|
return mValue.includes(fValue);
|
||||||
|
} else {
|
||||||
|
return mValue === fValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of keys in the map limited by, and in the order given by `keyOrKeys`.
|
||||||
|
*
|
||||||
|
* @param {MetadataMap} mdMap The source map.
|
||||||
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
|
*/
|
||||||
|
private static resolveKeys(mdMap: MetadataMap, keyOrKeys: string | string[]): string[] {
|
||||||
|
const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ];
|
||||||
|
const outputKeys: string[] = [];
|
||||||
|
for (const inputKey of inputKeys) {
|
||||||
|
if (inputKey.includes('*')) {
|
||||||
|
const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$');
|
||||||
|
for (const mapKey of Object.keys(mdMap)) {
|
||||||
|
if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) {
|
||||||
|
outputKeys.push(mapKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (mdMap.hasOwnProperty(inputKey) && !outputKeys.includes(inputKey)) {
|
||||||
|
outputKeys.push(inputKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputKeys;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,24 +0,0 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
|
||||||
import * as uuidv4 from 'uuid/v4';
|
|
||||||
|
|
||||||
export class Metadatum {
|
|
||||||
|
|
||||||
uuid: string = uuidv4();
|
|
||||||
/**
|
|
||||||
* The metadata field of this Metadatum
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
key: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The language of this Metadatum
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of this Metadatum
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
value: string;
|
|
||||||
}
|
|
@@ -97,7 +97,7 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
scheduler.schedule(() => source.pipe(getRequestFromRequestUUID(requestService)).subscribe());
|
scheduler.schedule(() => source.pipe(getRequestFromRequestUUID(requestService)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.getByUUID).toHaveBeenCalledWith(testRequestUUID);
|
expect(requestService.getByUUID).toHaveBeenCalledWith(testRequestUUID)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldn\'t return anything if there is no request matching the request uuid', () => {
|
it('shouldn\'t return anything if there is no request matching the request uuid', () => {
|
||||||
|
@@ -51,28 +51,18 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
} as TextMenuItemModel,
|
} as TextMenuItemModel,
|
||||||
index: 0
|
index: 0
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// id: 'browse_global_communities_and_collections',
|
||||||
|
// parentID: 'browse_global',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.browse_global_communities_and_collections',
|
||||||
|
// link: '#'
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: 'browse_global_communities_and_collections',
|
|
||||||
parentID: 'browse_global',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.browse_global_communities_and_collections',
|
|
||||||
link: '#'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'browse_global_global_by_issue_date',
|
|
||||||
parentID: 'browse_global',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.browse_global_by_issue_date',
|
|
||||||
link: '#'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
}, {
|
|
||||||
id: 'browse_global_global_by_title',
|
id: 'browse_global_global_by_title',
|
||||||
parentID: 'browse_global',
|
parentID: 'browse_global',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -94,6 +84,17 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/browse/author'
|
link: '/browse/author'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'browse_global_by_subject',
|
||||||
|
parentID: 'browse_global',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.browse_global_by_subject',
|
||||||
|
link: '/browse/subject'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
/* Statistics */
|
/* Statistics */
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngVar="(objects$ | async) as objects">
|
<ng-container *ngVar="(objects$ | async) as objects">
|
||||||
<h2 class="w-100">{{title}}</h2>
|
<h2 class="w-100">{{title | translate}}</h2>
|
||||||
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="paginationConfig"
|
[config]="paginationConfig"
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
[objects]="objects">
|
[objects]="objects">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-loading *ngIf="!objects || objects?.payload?.page.length <= 0" message="{{'loading.browse-by' | translate}}"></ds-loading>
|
<ds-loading *ngIf="!objects || objects?.isLoading" message="{{'loading.browse-by' | translate}}"></ds-loading>
|
||||||
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
|
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
|
||||||
|
<div *ngIf="!objects?.isLoading && objects?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'browse.empty' | translate}}
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -5,7 +5,6 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
|
|||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { fadeIn, fadeInOut } from '../animations/fade';
|
import { fadeIn, fadeInOut } from '../animations/fade';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -21,10 +20,23 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
|
|||||||
* Component to display a browse-by page for any ListableObject
|
* Component to display a browse-by page for any ListableObject
|
||||||
*/
|
*/
|
||||||
export class BrowseByComponent {
|
export class BrowseByComponent {
|
||||||
|
/**
|
||||||
|
* The i18n message to display as title
|
||||||
|
*/
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of objects to display
|
||||||
|
*/
|
||||||
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination configuration used for the list
|
||||||
|
*/
|
||||||
@Input() paginationConfig: PaginationComponentOptions;
|
@Input() paginationConfig: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting configuration used for the list
|
||||||
|
*/
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
@Input() currentUrl: string;
|
|
||||||
query: string;
|
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,6 @@ import { ResourceType } from '../../../core/shared/resource-type';
|
|||||||
import { ComColFormComponent } from './comcol-form.component';
|
import { ComColFormComponent } from './comcol-form.component';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { hasValue } from '../../empty.util';
|
import { hasValue } from '../../empty.util';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
|
||||||
|
|
||||||
describe('ComColFormComponent', () => {
|
describe('ComColFormComponent', () => {
|
||||||
let comp: ComColFormComponent<DSpaceObject>;
|
let comp: ComColFormComponent<DSpaceObject>;
|
||||||
@@ -29,23 +28,24 @@ describe('ComColFormComponent', () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum;
|
const dcTitle = 'dc.title';
|
||||||
const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum;
|
const dcRandom = 'dc.random';
|
||||||
const abstractMD = {
|
const dcAbstract = 'dc.description.abstract';
|
||||||
key: 'dc.description.abstract',
|
|
||||||
value: 'Community description'
|
const titleMD = { [dcTitle]: [ { value: 'Community Title', language: null } ] };
|
||||||
} as Metadatum;
|
const randomMD = { [dcRandom]: [ { value: 'Random metadata excluded from form', language: null } ] };
|
||||||
const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum;
|
const abstractMD = { [dcAbstract]: [ { value: 'Community description', language: null } ] };
|
||||||
|
const newTitleMD = { [dcTitle]: [ { value: 'New Community Title', language: null } ] };
|
||||||
const formModel = [
|
const formModel = [
|
||||||
new DynamicInputModel({
|
new DynamicInputModel({
|
||||||
id: 'title',
|
id: 'title',
|
||||||
name: newTitleMD.key,
|
name: dcTitle,
|
||||||
value: 'New Community Title'
|
value: newTitleMD[dcTitle][0].value
|
||||||
}),
|
}),
|
||||||
new DynamicInputModel({
|
new DynamicInputModel({
|
||||||
id: 'abstract',
|
id: 'abstract',
|
||||||
name: abstractMD.key,
|
name: dcAbstract,
|
||||||
value: abstractMD.value
|
value: abstractMD[dcAbstract][0].value
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -84,10 +84,10 @@ describe('ComColFormComponent', () => {
|
|||||||
comp.dso = Object.assign(
|
comp.dso = Object.assign(
|
||||||
new Community(),
|
new Community(),
|
||||||
{
|
{
|
||||||
metadata: [
|
metadata: {
|
||||||
titleMD,
|
...titleMD,
|
||||||
randomMD
|
...randomMD
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ describe('ComColFormComponent', () => {
|
|||||||
{},
|
{},
|
||||||
new Community(),
|
new Community(),
|
||||||
{
|
{
|
||||||
metadata: [
|
metadata: {
|
||||||
randomMD,
|
...newTitleMD,
|
||||||
newTitleMD,
|
...randomMD,
|
||||||
abstractMD
|
...abstractMD
|
||||||
],
|
},
|
||||||
type: ResourceType.Community
|
type: ResourceType.Community
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@@ -8,6 +8,7 @@ import { FormGroup } from '@angular/forms';
|
|||||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||||
import { isNotEmpty } from '../../empty.util';
|
import { isNotEmpty } from '../../empty.util';
|
||||||
import { ResourceType } from '../../../core/shared/resource-type';
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.formModel.forEach(
|
this.formModel.forEach(
|
||||||
(fieldModel: DynamicInputModel) => {
|
(fieldModel: DynamicInputModel) => {
|
||||||
fieldModel.value = this.dso.findMetadata(fieldModel.name);
|
fieldModel.value = this.dso.firstMetadataValue(fieldModel.name);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||||
@@ -77,20 +78,24 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks which new fields where added and sends the updated version of the DSO to the parent component
|
* Checks which new fields were added and sends the updated version of the DSO to the parent component
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
const metadata = this.formModel.map(
|
const formMetadata = new Object() as MetadataMap;
|
||||||
(fieldModel: DynamicInputModel) => {
|
this.formModel.forEach((fieldModel: DynamicInputModel) => {
|
||||||
return { key: fieldModel.name, value: fieldModel.value }
|
const value: MetadataValue = { value: fieldModel.value as string, language: null };
|
||||||
|
if (formMetadata.hasOwnProperty(fieldModel.name)) {
|
||||||
|
formMetadata[fieldModel.name].push(value);
|
||||||
|
} else {
|
||||||
|
formMetadata[fieldModel.name] = [ value ];
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key));
|
|
||||||
const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value));
|
|
||||||
|
|
||||||
const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata];
|
|
||||||
const updatedDSO = Object.assign({}, this.dso, {
|
const updatedDSO = Object.assign({}, this.dso, {
|
||||||
metadata: newMetadata,
|
metadata: {
|
||||||
|
...this.dso.metadata,
|
||||||
|
...formMetadata
|
||||||
|
},
|
||||||
type: ResourceType.Community
|
type: ResourceType.Community
|
||||||
});
|
});
|
||||||
this.submitForm.emit(updatedDSO);
|
this.submitForm.emit(updatedDSO);
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<h3>{{'browse.comcol.head' | translate}}</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a [routerLink]="['/browse/title']" [queryParams]="{scope: id}">{{'browse.comcol.by.title' | translate}}</a></li>
|
||||||
|
<li><a [routerLink]="['/browse/author']" [queryParams]="{scope: id}">{{'browse.comcol.by.author' | translate}}</a></li>
|
||||||
|
<li><a [routerLink]="['/browse/subject']" [queryParams]="{scope: id}">{{'browse.comcol.by.subject' | translate}}</a></li>
|
||||||
|
</ul>
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component to display the "Browse By" section of a Community or Collection page
|
||||||
|
* It expects the ID of the Community or Collection as input to be passed on as a scope
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-comcol-page-browse-by',
|
||||||
|
templateUrl: './comcol-page-browse-by.component.html',
|
||||||
|
})
|
||||||
|
export class ComcolPageBrowseByComponent {
|
||||||
|
/**
|
||||||
|
* The ID of the Community or Collection
|
||||||
|
*/
|
||||||
|
@Input() id: string;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="content" class="content-with-optional-title">
|
<div *ngIf="content" class="content-with-optional-title mb-2">
|
||||||
<h2 *ngIf="title">{{ title | translate }}</h2>
|
<h2 *ngIf="title">{{ title | translate }}</h2>
|
||||||
<div *ngIf="hasInnerHtml" [innerHtml]="content"></div>
|
<div *ngIf="hasInnerHtml" [innerHtml]="content"></div>
|
||||||
<div *ngIf="!hasInnerHtml">{{content}}</div>
|
<div *ngIf="!hasInnerHtml">{{content}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -237,10 +237,9 @@ export class FormComponent implements OnDestroy, OnInit {
|
|||||||
private keepSync(): void {
|
private keepSync(): void {
|
||||||
this.subs.push(this.formService.getFormData(this.formId)
|
this.subs.push(this.formService.getFormData(this.formId)
|
||||||
.subscribe((stateFormData) => {
|
.subscribe((stateFormData) => {
|
||||||
// if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) {
|
if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) {
|
||||||
// this.formGroup.setValue(stateFormData);
|
this.formGroup.setValue(stateFormData);
|
||||||
console.log(stateFormData);
|
}
|
||||||
// }
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -51,13 +51,14 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||||
type: 'bitstream',
|
type: 'bitstream',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: 'test_word.docx'
|
value: 'test_word.docx'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sizeBytes: 31302,
|
sizeBytes: 31302,
|
||||||
@@ -85,13 +86,14 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
type: 'bitstream',
|
type: 'bitstream',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: 'test_pdf.pdf'
|
value: 'test_pdf.pdf'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -100,98 +102,106 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.creator': [
|
||||||
key: 'dc.creator',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Doe, Jane'
|
value: 'Doe, Jane'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.date.accessioned',
|
'dc.date.accessioned': [
|
||||||
language: null,
|
{
|
||||||
value: '1650-06-26T19:58:25Z'
|
language: null,
|
||||||
},
|
value: '1650-06-26T19:58:25Z'
|
||||||
{
|
}
|
||||||
key: 'dc.date.available',
|
],
|
||||||
language: null,
|
'dc.date.available': [
|
||||||
value: '1650-06-26T19:58:25Z'
|
{
|
||||||
},
|
language: null,
|
||||||
{
|
value: '1650-06-26T19:58:25Z'
|
||||||
key: 'dc.date.issued',
|
}
|
||||||
language: null,
|
],
|
||||||
value: '1650-06-26'
|
'dc.date.issued': [
|
||||||
},
|
{
|
||||||
{
|
language: null,
|
||||||
key: 'dc.identifier.issn',
|
value: '1650-06-26'
|
||||||
language: 'en_US',
|
}
|
||||||
value: '123456789'
|
],
|
||||||
},
|
'dc.identifier.issn': [
|
||||||
{
|
{
|
||||||
key: 'dc.identifier.uri',
|
language: 'en_US',
|
||||||
language: null,
|
value: '123456789'
|
||||||
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
|
}
|
||||||
},
|
],
|
||||||
{
|
'dc.identifier.uri': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: null,
|
||||||
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
|
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.description.provenance',
|
'dc.description.abstract': [
|
||||||
language: 'en',
|
{
|
||||||
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
|
language: 'en_US',
|
||||||
},
|
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
|
||||||
{
|
}
|
||||||
key: 'dc.description.provenance',
|
],
|
||||||
language: 'en',
|
'dc.description.provenance': [
|
||||||
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
|
{
|
||||||
},
|
language: 'en',
|
||||||
{
|
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
|
||||||
key: 'dc.description.provenance',
|
},
|
||||||
language: 'en',
|
{
|
||||||
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
|
language: 'en',
|
||||||
},
|
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
|
||||||
{
|
},
|
||||||
key: 'dc.description.provenance',
|
{
|
||||||
language: 'en',
|
language: 'en',
|
||||||
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
|
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dc.language',
|
language: 'en',
|
||||||
language: 'en_US',
|
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
|
||||||
value: 'en'
|
}
|
||||||
},
|
],
|
||||||
{
|
'dc.language': [
|
||||||
key: 'dc.rights',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: '© Jane Doe'
|
value: 'en'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.subject',
|
'dc.rights': [
|
||||||
language: 'en_US',
|
{
|
||||||
value: 'keyword1'
|
language: 'en_US',
|
||||||
},
|
value: '© Jane Doe'
|
||||||
{
|
}
|
||||||
key: 'dc.subject',
|
],
|
||||||
language: 'en_US',
|
'dc.subject': [
|
||||||
value: 'keyword2'
|
{
|
||||||
},
|
language: 'en_US',
|
||||||
{
|
value: 'keyword1'
|
||||||
key: 'dc.subject',
|
},
|
||||||
language: 'en_US',
|
{
|
||||||
value: 'keyword3'
|
language: 'en_US',
|
||||||
},
|
value: 'keyword2'
|
||||||
{
|
},
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test PowerPoint Document'
|
value: 'keyword3'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.type',
|
'dc.title': [
|
||||||
language: 'en_US',
|
{
|
||||||
value: 'text'
|
language: 'en_US',
|
||||||
}
|
value: 'Test PowerPoint Document'
|
||||||
],
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
owningCollection: observableOf({
|
owningCollection: observableOf({
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
|
||||||
requestPending: false,
|
requestPending: false,
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { hasValue } from '../../../empty.util';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-abstract-object-element',
|
selector: 'ds-abstract-object-element',
|
||||||
@@ -11,8 +10,4 @@ export class AbstractListableElementComponent <T extends ListableObject> {
|
|||||||
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) {
|
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) {
|
||||||
this.object = listableObject as T;
|
this.object = listableObject as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValue(data) {
|
|
||||||
return hasValue(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -8,21 +8,25 @@ let collectionGridElementComponent: CollectionGridElementComponent;
|
|||||||
let fixture: ComponentFixture<CollectionGridElementComponent>;
|
let fixture: ComponentFixture<CollectionGridElementComponent>;
|
||||||
|
|
||||||
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CollectionGridElementComponent', () => {
|
describe('CollectionGridElementComponent', () => {
|
||||||
|
@@ -8,21 +8,25 @@ let communityGridElementComponent: CommunityGridElementComponent;
|
|||||||
let fixture: ComponentFixture<CommunityGridElementComponent>;
|
let fixture: ComponentFixture<CommunityGridElementComponent>;
|
||||||
|
|
||||||
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CommunityGridElementComponent', () => {
|
describe('CommunityGridElementComponent', () => {
|
||||||
|
@@ -5,19 +5,19 @@
|
|||||||
</ds-grid-thumbnail>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
<h4 class="card-title">{{object.firstMetadataValue('dc.title')}}</h4>
|
||||||
|
|
||||||
<ds-truncatable-part [id]="object.id" [minLines]="2">
|
<ds-truncatable-part [id]="object.id" [minLines]="2">
|
||||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
|
<p *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" class="item-authors card-text text-muted">
|
||||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
<span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
|
||||||
<span *ngIf="!last">; </span>
|
<span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
<span *ngIf="object.hasMetadata('dc.date.issued')" class="item-date">{{object.firstMetadataValue("dc.date.issued")}}</span>
|
||||||
</p>
|
</p>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
|
||||||
<ds-truncatable-part [id]="object.id" [minLines]="5">
|
<ds-truncatable-part [id]="object.id" [minLines]="5">
|
||||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
|
<p *ngIf="object.hasMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.firstMetadataValue("dc.description.abstract")}}</p>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
|
||||||
<div class="text-center pt-2">
|
<div class="text-center pt-2">
|
||||||
|
@@ -11,31 +11,37 @@ let fixture: ComponentFixture<ItemGridElementComponent>;
|
|||||||
|
|
||||||
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.contributor.author': [
|
||||||
key: 'dc.contributor.author',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Smith, Donald'
|
value: 'Smith, Donald'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.date.issued',
|
'dc.date.issued': [
|
||||||
language: null,
|
{
|
||||||
value: '2015-06-26'
|
language: null,
|
||||||
}]
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'This is just another title'
|
value: 'This is just another title'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.type',
|
'dc.type': [
|
||||||
language: null,
|
{
|
||||||
value: 'Article'
|
language: null,
|
||||||
}]
|
value: 'Article'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ItemGridElementComponent', () => {
|
describe('ItemGridElementComponent', () => {
|
||||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||||
mockCollectionWithAbstract.hitHighlights = [];
|
mockCollectionWithAbstract.hitHighlights = {};
|
||||||
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||||
mockCollectionWithoutAbstract.hitHighlights = [];
|
mockCollectionWithoutAbstract.hitHighlights = {};
|
||||||
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CollectionSearchResultGridElementComponent', () => {
|
describe('CollectionSearchResultGridElementComponent', () => {
|
||||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||||
mockCommunityWithAbstract.hitHighlights = [];
|
mockCommunityWithAbstract.hitHighlights = {};
|
||||||
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||||
mockCommunityWithoutAbstract.hitHighlights = [];
|
mockCommunityWithoutAbstract.hitHighlights = {};
|
||||||
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CommunitySearchResultGridElementComponent', () => {
|
describe('CommunitySearchResultGridElementComponent', () => {
|
||||||
|
@@ -8,20 +8,20 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="dso.findMetadata('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
<p *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
|
||||||
class="item-authors card-text text-muted">
|
class="item-authors card-text text-muted">
|
||||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1">
|
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1">
|
||||||
<span *ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span>
|
<span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{dso.firstMetadataValue('dc.date.issued')}}</span>
|
||||||
<span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
|
<span *ngFor="let author of dso.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
|
||||||
<span [innerHTML]="authorMd.value"></span>
|
<span [innerHTML]="author"></span>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</p>
|
</p>
|
||||||
<p class="item-abstract card-text">
|
<p class="item-abstract card-text">
|
||||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3">
|
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3">
|
||||||
<span [innerHTML]="getFirstValue('dc.description.abstract')"></span>
|
<span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</p>
|
</p>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
@@ -17,37 +17,43 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||||
mockItemWithAuthorAndDate.hitHighlights = [];
|
mockItemWithAuthorAndDate.hitHighlights = {};
|
||||||
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.contributor.author': [
|
||||||
key: 'dc.contributor.author',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Smith, Donald'
|
value: 'Smith, Donald'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.date.issued',
|
'dc.date.issued': [
|
||||||
language: null,
|
{
|
||||||
value: '2015-06-26'
|
language: null,
|
||||||
}]
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||||
mockItemWithoutAuthorAndDate.hitHighlights = [];
|
mockItemWithoutAuthorAndDate.hitHighlights = {};
|
||||||
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'This is just another title'
|
value: 'This is just another title'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.type',
|
'dc.type': [
|
||||||
language: null,
|
{
|
||||||
value: 'Article'
|
language: null,
|
||||||
}]
|
value: 'Article'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ItemSearchResultGridElementComponent', () => {
|
describe('ItemSearchResultGridElementComponent', () => {
|
||||||
|
@@ -2,12 +2,11 @@ import { Component, Inject } from '@angular/core';
|
|||||||
|
|
||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
|
||||||
import { isEmpty, hasNoValue, hasValue } from '../../empty.util';
|
|
||||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||||
import { TruncatableService } from '../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../truncatable/truncatable.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { Metadata } from '../../../core/shared/metadata.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-result-grid-element',
|
selector: 'ds-search-result-grid-element',
|
||||||
@@ -22,39 +21,24 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
|
|||||||
this.dso = this.object.dspaceObject;
|
this.dso = this.object.dspaceObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
getValues(keys: string[]): string[] {
|
/**
|
||||||
const results: string[] = new Array<string>();
|
* Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights.
|
||||||
this.object.hitHighlights.forEach(
|
*
|
||||||
(md: Metadatum) => {
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
if (keys.indexOf(md.key) > -1) {
|
* @returns {string[]} the matching string values or an empty array.
|
||||||
results.push(md.value);
|
*/
|
||||||
}
|
allMetadataValues(keyOrKeys: string | string[]): string[] {
|
||||||
}
|
return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
|
||||||
);
|
|
||||||
if (isEmpty(results)) {
|
|
||||||
this.dso.filterMetadata(keys).forEach(
|
|
||||||
(md: Metadatum) => {
|
|
||||||
results.push(md.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstValue(key: string): string {
|
/**
|
||||||
let result: string;
|
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
|
||||||
this.object.hitHighlights.some(
|
*
|
||||||
(md: Metadatum) => {
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
if (key === md.key) {
|
* @returns {string} the first matching string value, or `undefined`.
|
||||||
result = md.value;
|
*/
|
||||||
return true;
|
firstMetadataValue(keyOrKeys: string | string[]): string {
|
||||||
}
|
return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
|
||||||
}
|
|
||||||
);
|
|
||||||
if (hasNoValue(result)) {
|
|
||||||
result = this.dso.findMetadata(key);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollapsed(): Observable<boolean> {
|
isCollapsed(): Observable<boolean> {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
|
<a [routerLink]="" [queryParams]="{value: object.value}" [queryParamsHandling]="'merge'" class="lead">
|
||||||
{{object.value}}
|
{{object.value}}
|
||||||
</a>
|
</a>
|
||||||
<span class="pr-2"> </span>
|
<span class="pr-2"> </span>
|
||||||
|
@@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TruncatePipe } from '../../utils/truncate.pipe';
|
import { TruncatePipe } from '../../utils/truncate.pipe';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
|
||||||
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
|
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
|
||||||
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ describe('MetadataListElementComponent', () => {
|
|||||||
browseEntryListElementComponent = fixture.componentInstance;
|
browseEntryListElementComponent = fixture.componentInstance;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('When the metadatum is loaded', () => {
|
describe('When the metadata is loaded', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
browseEntryListElementComponent.object = mockValue;
|
browseEntryListElementComponent.object = mockValue;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@@ -8,21 +8,25 @@ let collectionListElementComponent: CollectionListElementComponent;
|
|||||||
let fixture: ComponentFixture<CollectionListElementComponent>;
|
let fixture: ComponentFixture<CollectionListElementComponent>;
|
||||||
|
|
||||||
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CollectionListElementComponent', () => {
|
describe('CollectionListElementComponent', () => {
|
||||||
|
@@ -8,21 +8,25 @@ let communityListElementComponent: CommunityListElementComponent;
|
|||||||
let fixture: ComponentFixture<CommunityListElementComponent>;
|
let fixture: ComponentFixture<CommunityListElementComponent>;
|
||||||
|
|
||||||
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CommunityListElementComponent', () => {
|
describe('CommunityListElementComponent', () => {
|
||||||
|
@@ -1,23 +1,23 @@
|
|||||||
<ds-truncatable [id]="object.id">
|
<ds-truncatable [id]="object.id">
|
||||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||||
{{object.findMetadata("dc.title")}}
|
{{object.firstMetadataValue("dc.title")}}
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<ds-truncatable-part [id]="object.id" [minLines]="1">
|
<ds-truncatable-part [id]="object.id" [minLines]="1">
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
<span *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
|
||||||
class="item-list-authors">
|
class="item-list-authors">
|
||||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
<span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
|
||||||
<span *ngIf="!last">; </span>
|
<span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
(<span *ngIf="object.hasMetadata('dc.publisher')" class="item-list-publisher">{{object.firstMetadataValue("dc.publisher")}}, </span><span
|
||||||
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
*ngIf="object.hasMetadata('dc.date.issued')" class="item-list-date">{{object.firstMetadataValue("dc.date.issued")}}</span>)
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<ds-truncatable-part [id]="object.id" [minLines]="3">
|
<ds-truncatable-part [id]="object.id" [minLines]="3">
|
||||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
<div *ngIf="object.hasMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||||
{{object.findMetadata("dc.description.abstract")}}
|
{{object.firstMetadataValue("dc.description.abstract")}}
|
||||||
</div>
|
</div>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,31 +11,37 @@ let fixture: ComponentFixture<ItemListElementComponent>;
|
|||||||
|
|
||||||
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.contributor.author': [
|
||||||
key: 'dc.contributor.author',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Smith, Donald'
|
value: 'Smith, Donald'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.date.issued',
|
'dc.date.issued': [
|
||||||
language: null,
|
{
|
||||||
value: '2015-06-26'
|
language: null,
|
||||||
}]
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'This is just another title'
|
value: 'This is just another title'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.type',
|
'dc.type': [
|
||||||
language: null,
|
{
|
||||||
value: 'Article'
|
language: null,
|
||||||
}]
|
value: 'Article'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ItemListElementComponent', () => {
|
describe('ItemListElementComponent', () => {
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
<a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
|
<a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||||
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="firstMetadataValue('dc.description.abstract')"></div>
|
||||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||||
mockCollectionWithAbstract.hitHighlights = [];
|
mockCollectionWithAbstract.hitHighlights = {};
|
||||||
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||||
mockCollectionWithoutAbstract.hitHighlights = [];
|
mockCollectionWithoutAbstract.hitHighlights = {};
|
||||||
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CollectionSearchResultListElementComponent', () => {
|
describe('CollectionSearchResultListElementComponent', () => {
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
<a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
|
<a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||||
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="firstMetadataValue('dc.description.abstract')"></div>
|
||||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||||
mockCommunityWithAbstract.hitHighlights = [];
|
mockCommunityWithAbstract.hitHighlights = {};
|
||||||
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.description.abstract': [
|
||||||
key: 'dc.description.abstract',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Short description'
|
value: 'Short description'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||||
mockCommunityWithoutAbstract.hitHighlights = [];
|
mockCommunityWithoutAbstract.hitHighlights = {};
|
||||||
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Test title'
|
value: 'Test title'
|
||||||
} ]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CommunitySearchResultListElementComponent', () => {
|
describe('CommunitySearchResultListElementComponent', () => {
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a
|
<a
|
||||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||||
[innerHTML]="getFirstValue('dc.title')"></a>
|
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher"
|
(<span *ngIf="dso.hasMetadata('dc.publisher')" class="item-list-publisher"
|
||||||
[innerHTML]="getFirstValue('dc.publisher') + ', '"></span><span
|
[innerHTML]="firstMetadataValue('dc.publisher') + ', '"></span><span
|
||||||
*ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-list-date"
|
*ngIf="dso.hasMetadata('dc.date.issued')" class="item-list-date"
|
||||||
[innerHTML]="getFirstValue('dc.date.issued')"></span>)
|
[innerHTML]="firstMetadataValue('dc.date.issued')"></span>)
|
||||||
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
<span *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
|
||||||
class="item-list-authors">
|
class="item-list-authors">
|
||||||
<span *ngFor="let author of getValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
|
<span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
|
||||||
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
|
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
<div *ngIf="dso.hasMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
||||||
[innerHTML]="getFirstValue('dc.description.abstract')"></span>
|
[innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</div>
|
</div>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -17,37 +17,43 @@ const truncatableServiceStub: any = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||||
mockItemWithAuthorAndDate.hitHighlights = [];
|
mockItemWithAuthorAndDate.hitHighlights = {};
|
||||||
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.contributor.author': [
|
||||||
key: 'dc.contributor.author',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'Smith, Donald'
|
value: 'Smith, Donald'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.date.issued',
|
'dc.date.issued': [
|
||||||
language: null,
|
{
|
||||||
value: '2015-06-26'
|
language: null,
|
||||||
}]
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||||
mockItemWithoutAuthorAndDate.hitHighlights = [];
|
mockItemWithoutAuthorAndDate.hitHighlights = {};
|
||||||
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||||
bitstreams: observableOf({}),
|
bitstreams: observableOf({}),
|
||||||
metadata: [
|
metadata: {
|
||||||
{
|
'dc.title': [
|
||||||
key: 'dc.title',
|
{
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
value: 'This is just another title'
|
value: 'This is just another title'
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
key: 'dc.type',
|
'dc.type': [
|
||||||
language: null,
|
{
|
||||||
value: 'Article'
|
language: null,
|
||||||
}]
|
value: 'Article'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ItemSearchResultListElementComponent', () => {
|
describe('ItemSearchResultListElementComponent', () => {
|
||||||
|
@@ -3,11 +3,10 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
|
||||||
import { hasNoValue, isEmpty } from '../../empty.util';
|
|
||||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
import { TruncatableService } from '../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../truncatable/truncatable.service';
|
||||||
|
import { Metadata } from '../../../core/shared/metadata.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-result-list-element',
|
selector: 'ds-search-result-list-element',
|
||||||
@@ -22,39 +21,24 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
|
|||||||
this.dso = this.object.dspaceObject;
|
this.dso = this.object.dspaceObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
getValues(keys: string[]): string[] {
|
/**
|
||||||
const results: string[] = new Array<string>();
|
* Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights.
|
||||||
this.object.hitHighlights.forEach(
|
*
|
||||||
(md: Metadatum) => {
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
if (keys.indexOf(md.key) > -1) {
|
* @returns {string[]} the matching string values or an empty array.
|
||||||
results.push(md.value);
|
*/
|
||||||
}
|
allMetadataValues(keyOrKeys: string | string[]): string[] {
|
||||||
}
|
return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
|
||||||
);
|
|
||||||
if (isEmpty(results)) {
|
|
||||||
this.dso.filterMetadata(keys).forEach(
|
|
||||||
(md: Metadatum) => {
|
|
||||||
results.push(md.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstValue(key: string): string {
|
/**
|
||||||
let result: string;
|
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
|
||||||
this.object.hitHighlights.some(
|
*
|
||||||
(md: Metadatum) => {
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||||
if (key === md.key) {
|
* @returns {string} the first matching string value, or `undefined`.
|
||||||
result = md.value;
|
*/
|
||||||
return true;
|
firstMetadataValue(keyOrKeys: string | string[]): string {
|
||||||
}
|
return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
|
||||||
}
|
|
||||||
);
|
|
||||||
if (hasNoValue(result)) {
|
|
||||||
result = this.dso.findMetadata(key);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollapsed(): Observable<boolean> {
|
isCollapsed(): Observable<boolean> {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user