Issues 252,253: Browse by title and browse by metadata (author)

This commit is contained in:
Kristof De Langhe
2018-08-08 11:36:42 +02:00
parent b6e4e2562d
commit 883a1d8881
41 changed files with 898 additions and 86 deletions

View File

@@ -135,6 +135,9 @@
}
}
},
"browse": {
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
@@ -193,7 +196,8 @@
"recent-submissions": "Loading recent submissions...",
"item": "Loading item...",
"objects": "Loading...",
"search-results": "Loading search results..."
"search-results": "Loading search results...",
"browse-by": "Loading items..."
},
"error": {
"default": "Error",
@@ -205,6 +209,7 @@
"item": "Error fetching item",
"objects": "Error fetching objects",
"search-results": "Error fetching search results",
"browse-by": "Error fetching items",
"validation": {
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
"license": {

View File

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

View File

@@ -0,0 +1,107 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../../core/shared/metadatum.model';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-author-page',
styleUrls: ['./browse-by-author-page.component.scss'],
templateUrl: './browse-by-author-page.component.html'
})
export class BrowseByAuthorPageComponent implements OnInit {
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-author-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
value = '';
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
const startsWith = +params.query || params.query || '';
this.value = +params.value || params.value || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
const searchOptions = {
pagination: pagination,
sort: sort,
startsWith: startsWith
};
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
}));
}
/**
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions,
* startsWith: string }
*/
updatePage(searchOptions) {
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
this.items$ = undefined;
}
/**
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions,
* startsWith: string }
* @param author The author's name for displaying items
*/
updatePageWithItems(searchOptions, author: string) {
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-title w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
[objects$]="items$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,85 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
@Component({
selector: 'ds-browse-by-title-page',
styleUrls: ['./browse-by-title-page.component.scss'],
templateUrl: './browse-by-title-page.component.html'
})
export class BrowseByTitlePageComponent implements OnInit {
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = +params.page || this.sortConfig.direction;
const startsWith = +params.query || params.query || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: params.sortField }
);
this.updatePage({
pagination: pagination,
sort: sort,
startsWith: startsWith
});
}));
}
updatePage(searchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
sort: searchOptions.sort,
startsWith: searchOptions.startsWith
});
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

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

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
@NgModule({
imports: [
BrowseByRoutingModule,
CommonModule,
SharedModule
],
declarations: [
BrowseByTitlePageComponent,
BrowseByAuthorPageComponent
],
providers: [
ItemDataService,
BrowseService
]
})
export class BrowseByModule {
}

View File

@@ -12,6 +12,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },

View File

@@ -16,19 +16,27 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models';
import {
BrowseEndpointRequest,
BrowseEntriesRequest,
BrowseItemsRequest,
GetRequest,
RestRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
configureRequest,
filterSuccessfulResponses,
filterSuccessfulResponses, getBrowseDefinitionLinks,
getRemoteDataPayload,
getRequestFromSelflink,
getResponseFromSelflink
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable()
export class BrowseService {
@@ -71,6 +79,8 @@ export class BrowseService {
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))),
distinctUntilChanged()
);
@@ -82,17 +92,7 @@ export class BrowseService {
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
const request$ = this.getBrowseDefinitions().pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
}),
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.entries),
hasValueOperator(),
@@ -124,6 +124,57 @@ export class BrowseService {
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
})),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
}
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(options.pagination)) {
args.push(`page=${options.pagination.currentPage - 1}`);
args.push(`size=${options.pagination.pageSize}`);
}
if (isNotEmpty(filterValue)) {
args.push(`filterValue=${filterValue}`);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
}
return href;
}),
map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService)
);
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<Item>) => Object.assign(list, {
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
})),
distinctUntilChanged()
);

View File

@@ -62,6 +62,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
const IMPORTS = [
CommonModule,
@@ -114,6 +115,7 @@ const PROVIDERS = [
ServerResponseService,
BrowseResponseParsingService,
BrowseEntriesResponseParsingService,
BrowseItemsResponseParsingService,
BrowseService,
ConfigResponseParsingService,
RouteService,

View File

@@ -0,0 +1,168 @@
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
describe('BrowseItemsResponseParsingService', () => {
let service: BrowseItemsResponseParsingService;
beforeEach(() => {
service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService());
});
describe('parse', () => {
const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items');
const validResponse = {
payload: {
_embedded: {
items: [
{
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
{
id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
handle: '10986/17475',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:33:42.526+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b'
}
}
}
]
},
_links: {
first: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items'
},
next: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2'
},
last: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2'
}
},
page: {
size: 2,
totalElements: 16,
totalPages: 8,
number: 0
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
payload: {
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseStatusCode = {
payload: {}, statusCode: '500'
} as DSpaceRESTV2Response;
it('should return a GenericSuccessResponse if data contains a valid browse items response', () => {
const response = service.parse(request, validResponse);
expect(response.constructor).toBe(GenericSuccessResponse);
});
it('should return an ErrorResponse if data contains an invalid browse entries response', () => {
const response = service.parse(request, invalidResponseNotAList);
expect(response.constructor).toBe(ErrorResponse);
});
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
const response = service.parse(request, invalidResponseStatusCode);
expect(response.constructor).toBe(ErrorResponse);
});
});
});

View File

@@ -0,0 +1,49 @@
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
import {
ErrorResponse,
GenericSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable()
export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = {
getConstructor: () => DSpaceObject
};
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(DSpaceObject);
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
{ statusText: data.statusCode }
)
);
}
}
}

View File

@@ -10,7 +10,7 @@ import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service';
import { FindByIDRequest } from './request.models';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -53,6 +53,10 @@ describe('ComColDataService', () => {
const EnvConfig = {} as GlobalConfig;
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID
});
const communitiesEndpoint = 'https://rest.api/core/communities';
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
@@ -99,7 +103,7 @@ describe('ComColDataService', () => {
);
}
describe('getScopedEndpoint', () => {
describe('getBrowseEndpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
});
@@ -113,7 +117,7 @@ describe('ComColDataService', () => {
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
@@ -129,13 +133,13 @@ describe('ComColDataService', () => {
});
it('should fetch the scope Community from the cache', () => {
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush();
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
});
it('should return the endpoint to fetch resources within the given scope', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--e-', { e: scopedEndpoint });
expect(result).toBeObservable(expected);
@@ -152,7 +156,7 @@ describe('ComColDataService', () => {
});
it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
expect(result).toBeObservable(expected);

View File

@@ -8,7 +8,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service';
import { FindByIDRequest } from './request.models';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -27,16 +27,16 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
* @return { Observable<string> }
* an Observable<string> containing the scoped URL
*/
public getScopedEndpoint(scopeID: string): Observable<string> {
if (isEmpty(scopeID)) {
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(this.linkPath);
} else {
const scopeCommunityHrefObs = this.cds.getEndpoint()
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID))
.filter((href: string) => isNotEmpty(href))
.take(1)
.do((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID);
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request);
});
@@ -48,9 +48,9 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
Observable.throw(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))),
successResponse
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID))
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(options.scopeID))
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
.filter((href) => isNotEmpty(href))
).distinctUntilChanged();

View File

@@ -20,17 +20,13 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract linkPath: string;
protected abstract halService: HALEndpointService;
public abstract getScopedEndpoint(scope: string): Observable<string>
public abstract getBrowseEndpoint(options: FindAllOptions): Observable<string>
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
protected getFindAllHref(options: FindAllOptions = {}): Observable<string> {
let result: Observable<string>;
const args = [];
if (hasValue(options.scopeID)) {
result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged();
} else {
result = Observable.of(endpoint);
}
result = this.getBrowseEndpoint(options).distinctUntilChanged();
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
@@ -53,8 +49,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
}
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
const hrefObs = this.getFindAllHref(options);
hrefObs
.filter((href: string) => hasValue(href))

View File

@@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
describe('ItemDataService', () => {
let scheduler: TestScheduler;
@@ -20,9 +21,19 @@ describe('ItemDataService', () => {
const halEndpointService = {} as HALEndpointService;
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
const startsWith = 'a';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID,
sort: {
field: '',
direction: undefined
},
startsWith: startsWith
});
const browsesEndpoint = 'https://rest.api/discover/browses';
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}&startsWith=${startsWith}`;
const serviceEndpoint = `https://rest.api/core/items`;
const browseError = new Error('getBrowseURL failed');
@@ -46,16 +57,16 @@ describe('ItemDataService', () => {
);
}
describe('getScopedEndpoint', () => {
describe('getBrowseEndpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
});
it('should return the endpoint to fetch Items within the given scope', () => {
it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
bs = initMockBrowseService(true);
service = initTestService();
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--b-', { b: scopedEndpoint });
expect(result).toBeObservable(expected);
@@ -67,7 +78,7 @@ describe('ItemDataService', () => {
service = initTestService();
});
it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, browseError);
expect(result).toBeObservable(expected);

View File

@@ -15,6 +15,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -30,15 +31,15 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
super();
}
public getScopedEndpoint(scopeID: string): Observable<string> {
if (isEmpty(scopeID)) {
return this.halService.getEndpoint(this.linkPath);
} else {
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath)
.filter((href: string) => isNotEmpty(href))
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
.distinctUntilChanged();
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
let field = 'dc.date.issued';
if (options.sort && options.sort.field) {
field = options.sort.field;
}
return this.bs.getBrowseURLFor(field, this.linkPath)
.filter((href: string) => isNotEmpty(href))
.map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}` + (options.startsWith ? `&startsWith=${options.startsWith}` : '')).toString())
.distinctUntilChanged();
}
}

View File

@@ -12,6 +12,7 @@ import { AuthResponseParsingService } from '../auth/auth-response-parsing.servic
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
/* tslint:disable:max-classes-per-file */
@@ -141,6 +142,7 @@ export class FindAllOptions {
elementsPerPage?: number;
currentPage?: number;
sort?: SortOptions;
startsWith?: string;
}
export class FindAllRequest extends GetRequest {
@@ -183,6 +185,12 @@ export class BrowseEntriesRequest extends GetRequest {
}
}
export class BrowseItemsRequest extends GetRequest {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return BrowseItemsResponseParsingService;
}
}
export class ConfigRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);

View File

@@ -1,6 +1,7 @@
import { autoserialize, autoserializeAs } from 'cerialize';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
export class BrowseEntry {
export class BrowseEntry implements ListableObject {
@autoserialize
type: string;

View File

@@ -5,6 +5,7 @@ import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { Observable } from 'rxjs/Observable';
import { autoserialize } from 'cerialize';
/**
* An abstract model class for a DSpaceObject.
@@ -16,11 +17,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* The human-readable identifier of this DSpaceObject
*/
@autoserialize
id: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserialize
uuid: string;
/**
@@ -31,11 +34,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* The name for this DSpaceObject
*/
@autoserialize
name: string;
/**
* An array containing all metadata of this DSpaceObject
*/
@autoserialize
metadata: Metadatum[];
/**

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { filter, flatMap, map, tap } from 'rxjs/operators';
import { hasValueOperator } from '../../shared/empty.util';
import { hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { DSOSuccessResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
@@ -8,6 +8,7 @@ import { RemoteData } from '../data/remote-data';
import { RestRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from './browse-definition.model';
/**
* This file contains custom RxJS operators that can be used in multiple places
@@ -45,3 +46,19 @@ export const configureRequest = (requestService: RequestService) =>
export const getRemoteDataPayload = () =>
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
export const getBrowseDefinitionLinks = (definitionID: string) =>
(source: Observable<RemoteData<BrowseDefinition[]>>): Observable<any> =>
source.pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
})
);

View File

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

View File

@@ -0,0 +1,44 @@
import { BrowseByComponent } from './browse-by.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { SharedModule } from '../shared.module';
describe('BrowseByComponent', () => {
let comp: BrowseByComponent;
let fixture: ComponentFixture<BrowseByComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule],
declarations: [],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByComponent);
comp = fixture.componentInstance;
});
it('should display a loading message when objects is empty',() => {
(comp as any).objects = undefined;
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-loading'))).toBeDefined();
});
it('should display results when objects is not empty', () => {
(comp as any).objects = Observable.of({
payload: {
page: {
length: 1
}
}
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined();
});
});

View File

@@ -0,0 +1,27 @@
import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { Observable } from 'rxjs/Observable';
import { Item } from '../../core/shared/item.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
@Component({
selector: 'ds-browse-by',
styleUrls: ['./browse-by.component.scss'],
templateUrl: './browse-by.component.html',
animations: [
fadeIn,
fadeInOut
]
})
export class BrowseByComponent {
@Input() title: string;
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
@Input() paginationConfig: PaginationComponentOptions;
@Input() sortConfig: SortOptions;
@Input() currentUrl: string;
query: string;
}

View File

@@ -8,7 +8,7 @@
<ds-object-grid [config]="config"
[sortConfig]="sortConfig"
[objects]="objects"
[hideGear]="true"
[hideGear]="hideGear"
*ngIf="getViewMode()===viewModeEnum.Grid">
</ds-object-grid>

View File

@@ -1,22 +1,28 @@
<div class="card">
<ds-truncatable [id]="object.id">
<div class="card">
<a [routerLink]="['/items/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
</ds-grid-thumbnail>
</a>
<div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
<ds-truncatable-part [id]="object.id" [minLines]="2">
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
</p>
</ds-truncatable-part>
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
<ds-truncatable-part [id]="object.id" [minLines]="5">
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
</ds-truncatable-part>
<div class="text-center">
<div class="text-center pt-2">
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
</div>
</div>
</div>
</div>
</ds-truncatable>

View File

@@ -0,0 +1,3 @@
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
{{object.value}}
</a>

View File

@@ -0,0 +1 @@
@import '../../../../styles/variables';

View File

@@ -0,0 +1,47 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
let browseEntryListElementComponent: BrowseEntryListElementComponent;
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
type: 'browseEntry',
value: 'De Langhe Kristof'
});
describe('MetadataListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BrowseEntryListElementComponent , TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: {mockValue}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(BrowseEntryListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(BrowseEntryListElementComponent);
browseEntryListElementComponent = fixture.componentInstance;
}));
describe('When the metadatum is loaded', () => {
beforeEach(() => {
browseEntryListElementComponent.object = mockValue;
fixture.detectChanges();
});
it('should show the value as a link', () => {
const browseEntryLink = fixture.debugElement.query(By.css('a.lead'));
expect(browseEntryLink.nativeElement.textContent.trim()).toBe(mockValue.value);
});
});
});

View File

@@ -0,0 +1,17 @@
import { Component, Input, Inject } from '@angular/core';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
import { ActivatedRoute, Router } from '@angular/router';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
@Component({
selector: 'ds-browse-entry-list-element',
styleUrls: ['./browse-entry-list-element.component.scss'],
templateUrl: './browse-entry-list-element.component.html'
})
@renderElementsFor(BrowseEntry, ViewMode.List)
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {}

View File

@@ -1,7 +1,9 @@
<a [routerLink]="['/items/' + object.id]" class="lead">
<ds-truncatable [id]="object.id">
<a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}}
</a>
<div>
</a>
<div>
<ds-truncatable-part [id]="object.id" [minLines]="1">
<span class="text-muted">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-list-authors">
@@ -12,7 +14,11 @@
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
</span>
</ds-truncatable-part>
<ds-truncatable-part [id]="object.id" [minLines]="3">
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
{{object.findMetadata("dc.description.abstract")}}
</div>
</div>
</ds-truncatable-part>
</div>
</ds-truncatable>

View File

@@ -0,0 +1,3 @@
<a [routerLink]="['/browse/' + object.key + '/' + object.value]" class="lead">
{{object.value}}
</a>

View File

@@ -0,0 +1 @@
@import '../../../../styles/variables';

View File

@@ -0,0 +1,48 @@
import { MetadataListElementComponent } from './metadata-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/Observable';
import { Metadatum } from '../../../core/shared/metadatum.model';
let metadataListElementComponent: MetadataListElementComponent;
let fixture: ComponentFixture<MetadataListElementComponent>;
const mockValue: Metadatum = Object.assign(new Metadatum(), {
key: 'dc.contributor.author',
value: 'De Langhe Kristof'
});
describe('MetadataListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MetadataListElementComponent , TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: {mockValue}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(MetadataListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(MetadataListElementComponent);
metadataListElementComponent = fixture.componentInstance;
}));
describe('When the metadatum is loaded', () => {
beforeEach(() => {
metadataListElementComponent.object = mockValue;
fixture.detectChanges();
});
it('should show the value as a link', () => {
const metadatumLink = fixture.debugElement.query(By.css('a.lead'));
expect(metadatumLink.nativeElement.textContent.trim()).toBe(mockValue.value);
});
});
});

View File

@@ -0,0 +1,15 @@
import { Component, Input, Inject } from '@angular/core';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model';
import { Metadatum } from '../../../core/shared/metadatum.model';
@Component({
selector: 'ds-metadata-list-element',
styleUrls: ['./metadata-list-element.component.scss'],
templateUrl: './metadata-list-element.component.html'
})
@renderElementsFor(Metadatum, ViewMode.List)
export class MetadataListElementComponent extends AbstractListableElementComponent<Metadatum> {}

View File

@@ -72,6 +72,10 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { MetadataListElementComponent } from './object-list/metadata-list-element/metadata-list-element.component';
import { BrowseByModule } from '../+browse-by/browse-by.module';
import { BrowseByComponent } from './browse-by/browse-by.component';
import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -139,11 +143,13 @@ const COMPONENTS = [
ViewModeSwitchComponent,
TruncatableComponent,
TruncatablePartComponent,
BrowseByComponent
];
const ENTRY_COMPONENTS = [
// put shared entry components (components that are created dynamically) here
ItemListElementComponent,
MetadataListElementComponent,
CollectionListElementComponent,
CommunityListElementComponent,
SearchResultListElementComponent,
@@ -151,6 +157,7 @@ const ENTRY_COMPONENTS = [
CollectionGridElementComponent,
CommunityGridElementComponent,
SearchResultGridElementComponent,
BrowseEntryListElementComponent
];
const PROVIDERS = [