mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-15 05:53:03 +00:00
Merge branch 'master' into resolvers-branch-angular6
Conflicts: package.json src/app/+search-page/search-filters/search-filters.component.ts src/app/core/auth/auth.effects.ts src/app/core/auth/auth.service.ts src/app/core/auth/server-auth.service.ts src/app/core/data/comcol-data.service.ts src/app/core/data/community-data.service.ts src/app/core/data/data.service.ts src/app/core/data/item-data.service.ts src/app/core/shared/dspace-object.model.ts src/app/header/header.component.spec.ts src/app/shared/auth-nav-menu/auth-nav-menu.component.ts src/app/shared/testing/auth-service-stub.ts yarn.lock
This commit is contained in:
@@ -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)? '"' + value + '"': ''} }}"
|
||||
[objects$]="(items$ !== undefined)? items$ : authors$"
|
||||
[currentUrl]="currentUrl"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig">
|
||||
</ds-browse-by>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,108 @@
|
||||
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'
|
||||
})
|
||||
/**
|
||||
* 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(
|
||||
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;
|
||||
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,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>
|
@@ -0,0 +1,92 @@
|
||||
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'
|
||||
})
|
||||
/**
|
||||
* Component for browsing items by title (dc.title)
|
||||
*/
|
||||
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.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
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions }
|
||||
*/
|
||||
updatePage(searchOptions) {
|
||||
this.items$ = this.itemDataService.findAll({
|
||||
currentPage: searchOptions.pagination.currentPage,
|
||||
elementsPerPage: searchOptions.pagination.pageSize,
|
||||
sort: searchOptions.sort
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
16
src/app/+browse-by/browse-by-routing.module.ts
Normal file
16
src/app/+browse-by/browse-by-routing.module.ts
Normal 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 {
|
||||
|
||||
}
|
27
src/app/+browse-by/browse-by.module.ts
Normal file
27
src/app/+browse-by/browse-by.module.ts
Normal 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 {
|
||||
|
||||
}
|
@@ -35,7 +35,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed"0 message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
|
||||
<br>
|
||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||
|
@@ -6,13 +6,21 @@ import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific collection before the route is activated
|
||||
*/
|
||||
@Injectable()
|
||||
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
||||
constructor(private collectionService: CollectionDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a collection based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
||||
|
||||
return this.collectionService.findById(route.params.id).pipe(
|
||||
getSucceededRemoteData()
|
||||
);
|
||||
|
@@ -6,13 +6,21 @@ import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific community before the route is activated
|
||||
*/
|
||||
@Injectable()
|
||||
export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
||||
constructor(private communityService: CommunityDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a community based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
||||
|
||||
return this.communityService.findById(route.params.id).pipe(
|
||||
getSucceededRemoteData()
|
||||
);
|
||||
|
@@ -6,11 +6,20 @@ import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { ItemDataService } from '../core/data/item-data.service';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific item before the route is activated
|
||||
*/
|
||||
@Injectable()
|
||||
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||
constructor(private itemService: ItemDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving an item based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||
return this.itemService.findById(route.params.id).pipe(
|
||||
getSucceededRemoteData()
|
||||
|
@@ -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' },
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { AuthType } from './auth-type';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
|
||||
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
|
||||
export class AuthObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
|
||||
public static getConstructor(type): GenericConstructor<NormalizedObject> {
|
||||
switch (type) {
|
||||
case AuthType.Eperson: {
|
||||
return NormalizedEpersonModel
|
||||
case AuthType.EPerson: {
|
||||
return NormalizedEPerson
|
||||
}
|
||||
|
||||
case AuthType.Status: {
|
||||
|
@@ -8,12 +8,13 @@ import { CoreState } from '../core.reducers';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthResponseParsingService } from './auth-response-parsing.service';
|
||||
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
|
||||
import { getMockStore } from '../../shared/mocks/mock-store';
|
||||
|
||||
describe('ConfigResponseParsingService', () => {
|
||||
describe('AuthResponseParsingService', () => {
|
||||
let service: AuthResponseParsingService;
|
||||
|
||||
const EnvConfig = {} as GlobalConfig;
|
||||
const store = {} as Store<CoreState>;
|
||||
const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig;
|
||||
const store = getMockStore() as Store<CoreState>;
|
||||
const objectCacheService = new ObjectCacheService(store);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -86,13 +87,19 @@ describe('ConfigResponseParsingService', () => {
|
||||
type: 'eperson',
|
||||
uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||
_links: {
|
||||
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
|
||||
self: {
|
||||
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_links: {
|
||||
eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
|
||||
eperson: {
|
||||
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
|
||||
},
|
||||
self: {
|
||||
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
|
||||
}
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
|
@@ -12,22 +12,23 @@ import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RestRequest } from '../data/request.models';
|
||||
import { AuthType } from './auth-type';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||
|
||||
@Injectable()
|
||||
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
|
||||
protected objectFactory = AuthObjectFactory;
|
||||
protected toCache = false;
|
||||
protected toCache = true;
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,) {
|
||||
protected objectCache: ObjectCacheService) {
|
||||
super();
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
|
||||
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
|
||||
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
|
||||
return new AuthStatusResponse(response, data.statusCode);
|
||||
} else {
|
||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export enum AuthType {
|
||||
Eperson = 'eperson',
|
||||
EPerson = 'eperson',
|
||||
Status = 'status'
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { Action } from '@ngrx/store';
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
|
||||
// import models
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
|
||||
export const AuthActionTypes = {
|
||||
@@ -76,10 +76,10 @@ export class AuthenticatedSuccessAction implements Action {
|
||||
payload: {
|
||||
authenticated: boolean;
|
||||
authToken: AuthTokenInfo;
|
||||
user: Eperson
|
||||
user: EPerson
|
||||
};
|
||||
|
||||
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) {
|
||||
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) {
|
||||
this.payload = { authenticated, authToken, user };
|
||||
}
|
||||
}
|
||||
@@ -250,9 +250,9 @@ export class RefreshTokenErrorAction implements Action {
|
||||
*/
|
||||
export class RegistrationAction implements Action {
|
||||
public type: string = AuthActionTypes.REGISTRATION;
|
||||
payload: Eperson;
|
||||
payload: EPerson;
|
||||
|
||||
constructor(user: Eperson) {
|
||||
constructor(user: EPerson) {
|
||||
this.payload = user;
|
||||
}
|
||||
}
|
||||
@@ -278,9 +278,9 @@ export class RegistrationErrorAction implements Action {
|
||||
*/
|
||||
export class RegistrationSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
|
||||
payload: Eperson;
|
||||
payload: EPerson;
|
||||
|
||||
constructor(user: Eperson) {
|
||||
constructor(user: EPerson) {
|
||||
this.payload = user;
|
||||
}
|
||||
}
|
||||
|
@@ -25,12 +25,11 @@ import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||
import { AuthService } from './auth.service';
|
||||
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||
|
||||
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
|
||||
describe('AuthEffects', () => {
|
||||
let authEffects: AuthEffects;
|
||||
let actions: Observable<any>;
|
||||
|
||||
const authServiceStub = new AuthServiceStub();
|
||||
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
@@ -105,7 +104,7 @@ describe('AuthEffects', () => {
|
||||
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
|
||||
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
|
||||
|
||||
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)});
|
||||
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)});
|
||||
|
||||
expect(authEffects.authenticated$).toBeObservable(expected);
|
||||
});
|
||||
|
@@ -28,7 +28,7 @@ import {
|
||||
RegistrationErrorAction,
|
||||
RegistrationSuccessAction
|
||||
} from './auth.actions';
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
@@ -66,7 +66,7 @@ export class AuthEffects {
|
||||
ofType(AuthActionTypes.AUTHENTICATED),
|
||||
switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser(action.payload).pipe(
|
||||
map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
|
||||
map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||
})
|
||||
);
|
||||
@@ -94,7 +94,7 @@ export class AuthEffects {
|
||||
debounceTime(500), // to remove when functionality is implemented
|
||||
switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload).pipe(
|
||||
map((user: Eperson) => new RegistrationSuccessAction(user)),
|
||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
|
@@ -42,7 +42,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
}
|
||||
|
||||
private isSuccess(response: HttpResponseBase): boolean {
|
||||
return response.status === 200;
|
||||
return (response.status === 200 || response.status === 204);
|
||||
}
|
||||
|
||||
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
|
||||
describe('authReducer', () => {
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('authReducer', () => {
|
||||
loading: true,
|
||||
info: undefined
|
||||
};
|
||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock);
|
||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock);
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: true,
|
||||
@@ -116,7 +116,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -182,7 +182,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
|
||||
const action = new LogOutAction();
|
||||
@@ -199,7 +199,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
|
||||
const action = new LogOutSuccessAction();
|
||||
@@ -225,7 +225,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
|
||||
const action = new LogOutErrorAction(mockError);
|
||||
@@ -237,7 +237,7 @@ describe('authReducer', () => {
|
||||
error: 'Test error message',
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -250,7 +250,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||
const action = new RefreshTokenAction(newTokenInfo);
|
||||
@@ -262,7 +262,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock,
|
||||
user: EPersonMock,
|
||||
refreshing: true
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
@@ -276,7 +276,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock,
|
||||
user: EPersonMock,
|
||||
refreshing: true
|
||||
};
|
||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||
@@ -289,7 +289,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock,
|
||||
user: EPersonMock,
|
||||
refreshing: false
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
@@ -303,7 +303,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock,
|
||||
user: EPersonMock,
|
||||
refreshing: true
|
||||
};
|
||||
const action = new RefreshTokenErrorAction();
|
||||
@@ -329,7 +329,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
// import models
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ export interface AuthState {
|
||||
refreshing?: boolean;
|
||||
|
||||
// the authenticated user
|
||||
user?: Eperson;
|
||||
user?: EPerson;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -17,10 +17,12 @@ import { AuthRequestServiceStub } from '../../shared/testing/auth-request-servic
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { ClientCookieService } from '../../shared/services/client-cookie.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||
|
||||
describe('AuthService test', () => {
|
||||
|
||||
@@ -41,9 +43,9 @@ describe('AuthService test', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: token,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
|
||||
const rdbService = getMockRemoteDataBuildService();
|
||||
describe('', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -60,6 +62,7 @@ describe('AuthService test', () => {
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Store, useValue: mockStore},
|
||||
{provide: RemoteDataBuildService, useValue: rdbService},
|
||||
CookieService,
|
||||
AuthService
|
||||
],
|
||||
@@ -78,7 +81,7 @@ describe('AuthService test', () => {
|
||||
});
|
||||
|
||||
it('should return the authenticated user object when user token is valid', () => {
|
||||
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => {
|
||||
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => {
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -120,6 +123,7 @@ describe('AuthService test', () => {
|
||||
{provide: AuthRequestService, useValue: authRequest},
|
||||
{provide: REQUEST, useValue: {}},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: RemoteDataBuildService, useValue: rdbService},
|
||||
CookieService
|
||||
]
|
||||
}).compileComponents();
|
||||
@@ -131,7 +135,7 @@ describe('AuthService test', () => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, authReqService, router, cookieService, store);
|
||||
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
|
||||
}));
|
||||
|
||||
it('should return true when user is logged in', () => {
|
||||
@@ -183,14 +187,14 @@ describe('AuthService test', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: expiredToken,
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, authReqService, router, cookieService, store);
|
||||
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
|
||||
storage = (authService as any).storage;
|
||||
spyOn(storage, 'get');
|
||||
spyOn(storage, 'remove');
|
||||
|
@@ -16,8 +16,10 @@ import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||
import { RouterReducerState } from '@ngrx/router-store';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { CookieAttributes } from 'js-cookie';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
@@ -35,6 +37,8 @@ import { AppState, routerStateSelector } from '../../app.reducer';
|
||||
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
|
||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
|
||||
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
export const LOGOUT_ROUTE = '/logout';
|
||||
@@ -58,7 +62,9 @@ export class AuthService {
|
||||
protected authRequestService: AuthRequestService,
|
||||
protected router: Router,
|
||||
protected storage: CookieService,
|
||||
protected store: Store<AppState>) {
|
||||
protected store: Store<AppState>,
|
||||
protected rdbService: RemoteDataBuildService
|
||||
) {
|
||||
this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
startWith(false)
|
||||
@@ -132,7 +138,7 @@ export class AuthService {
|
||||
* Returns the authenticated user
|
||||
* @returns {User}
|
||||
*/
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
@@ -140,13 +146,18 @@ export class AuthService {
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
options.headers = headers;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => {
|
||||
switchMap((status: AuthStatus) => {
|
||||
|
||||
if (status.authenticated) {
|
||||
return status.eperson[0];
|
||||
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
||||
// Review when https://jira.duraspace.org/browse/DS-4006 is fixed
|
||||
// See https://github.com/DSpace/dspace-angular/issues/292
|
||||
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
||||
return person$.pipe(map((eperson) => eperson.payload));
|
||||
} else {
|
||||
throw(new Error('Not authenticated'));
|
||||
}
|
||||
}));
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,7 +217,7 @@ export class AuthService {
|
||||
* Create a new user
|
||||
* @returns {User}
|
||||
*/
|
||||
public create(user: Eperson): Observable<Eperson> {
|
||||
public create(user: EPerson): Observable<EPerson> {
|
||||
// Normally you would do an HTTP request to POST the user
|
||||
// details and then return the new user object
|
||||
// but, let's just return the new user for this example.
|
||||
@@ -357,8 +368,12 @@ export class AuthService {
|
||||
this.router.navigated = false;
|
||||
const url = decodeURIComponent(redirectUrl);
|
||||
this.router.navigateByUrl(url);
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = url;
|
||||
} else {
|
||||
this.router.navigate(['/']);
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = '/';
|
||||
}
|
||||
})
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { AuthError } from './auth-error.model';
|
||||
import { AuthTokenInfo } from './auth-token-info.model';
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { Eperson } from '../../eperson/models/eperson.model';
|
||||
import { EPerson } from '../../eperson/models/eperson.model';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
export class AuthStatus {
|
||||
|
||||
@@ -13,7 +14,7 @@ export class AuthStatus {
|
||||
|
||||
error?: AuthError;
|
||||
|
||||
eperson: Eperson[];
|
||||
eperson: Observable<RemoteData<EPerson>>;
|
||||
|
||||
token?: AuthTokenInfo;
|
||||
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { AuthStatus } from './auth-status.model';
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||
import { Eperson } from '../../eperson/models/eperson.model';
|
||||
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { NormalizedObject } from '../../cache/models/normalized-object.model';
|
||||
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
|
||||
|
||||
@mapsTo(AuthStatus)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
|
||||
@inheritSerialization(NormalizedObject)
|
||||
export class NormalizedAuthStatus extends NormalizedObject {
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
@autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* True if REST API is up and running, should never return false
|
||||
@@ -20,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject {
|
||||
@autoserialize
|
||||
authenticated: boolean;
|
||||
|
||||
@autoserializeAs(Eperson)
|
||||
eperson: Eperson[];
|
||||
|
||||
@relationship(ResourceType.EPerson, false)
|
||||
@autoserialize
|
||||
eperson: string;
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
|
||||
import {first, map} from 'rxjs/operators';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -10,7 +9,8 @@ import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { CheckAuthenticationTokenAction } from './auth.actions';
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
|
||||
|
||||
/**
|
||||
* The auth service.
|
||||
@@ -22,7 +22,7 @@ export class ServerAuthService extends AuthService {
|
||||
* Returns the authenticated user
|
||||
* @returns {User}
|
||||
*/
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
@@ -35,13 +35,18 @@ export class ServerAuthService extends AuthService {
|
||||
|
||||
options.headers = headers;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => {
|
||||
switchMap((status: AuthStatus) => {
|
||||
|
||||
if (status.authenticated) {
|
||||
return status.eperson[0];
|
||||
|
||||
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
||||
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
||||
// person$.subscribe(() => console.log('test'));
|
||||
return person$.pipe(map((eperson) => eperson.payload));
|
||||
} else {
|
||||
throw(new Error('Not authenticated'));
|
||||
}
|
||||
}));
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,7 +6,7 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models';
|
||||
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { BrowseService } from './browse.service';
|
||||
@@ -143,7 +143,9 @@ describe('BrowseService', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('getBrowseEntriesFor', () => {
|
||||
describe('getBrowseEntriesFor and getBrowseItemsFor', () => {
|
||||
const mockAuthorName = 'Donald Smith';
|
||||
|
||||
beforeEach(() => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
@@ -156,7 +158,7 @@ describe('BrowseService', () => {
|
||||
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||
});
|
||||
|
||||
describe('when called with a valid browse definition id', () => {
|
||||
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
|
||||
it('should configure a new BrowseEntriesRequest', () => {
|
||||
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
||||
|
||||
@@ -175,7 +177,26 @@ describe('BrowseService', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('when called with an invalid browse definition id', () => {
|
||||
describe('when getBrowseItemsFor is called with a valid browse definition id', () => {
|
||||
it('should configure a new BrowseItemsRequest', () => {
|
||||
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
|
||||
|
||||
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
|
||||
|
||||
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => {
|
||||
it('should throw an Error', () => {
|
||||
|
||||
const definitionID = 'invalidID';
|
||||
@@ -184,6 +205,16 @@ describe('BrowseService', () => {
|
||||
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getBrowseItemsFor is called with an invalid browse definition id', () => {
|
||||
it('should throw an Error', () => {
|
||||
|
||||
const definitionID = 'invalidID';
|
||||
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||
|
||||
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrowseURLFor', () => {
|
||||
|
@@ -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,66 @@ 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$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items linked to a certain metadata value
|
||||
* @param {string} definitionID definition ID to define the metadata-field (e.g. author)
|
||||
* @param {string} filterValue metadata value to filter by (e.g. author's name)
|
||||
* @param options Options to narrow down your search:
|
||||
* { pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions }
|
||||
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
||||
*/
|
||||
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()
|
||||
);
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import { ResourceType } from '../../shared/resource-type';
|
||||
import { NormalizedObject } from './normalized-object.model';
|
||||
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
||||
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
|
||||
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
|
||||
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
|
||||
|
||||
export class NormalizedObjectFactory {
|
||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
||||
@@ -33,6 +35,12 @@ export class NormalizedObjectFactory {
|
||||
case ResourceType.ResourcePolicy: {
|
||||
return NormalizedResourcePolicy
|
||||
}
|
||||
case ResourceType.EPerson: {
|
||||
return NormalizedEPerson
|
||||
}
|
||||
case ResourceType.Group: {
|
||||
return NormalizedGroup
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -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';
|
||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||
|
||||
const IMPORTS = [
|
||||
@@ -115,6 +116,7 @@ const PROVIDERS = [
|
||||
ServerResponseService,
|
||||
BrowseResponseParsingService,
|
||||
BrowseEntriesResponseParsingService,
|
||||
BrowseItemsResponseParsingService,
|
||||
BrowseService,
|
||||
ConfigResponseParsingService,
|
||||
RouteService,
|
||||
|
@@ -7,6 +7,8 @@ import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||
|
||||
function isObjectLevel(halObj: any) {
|
||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||
@@ -34,6 +36,7 @@ export abstract class BaseResponseParsingService {
|
||||
} else if (Array.isArray(data)) {
|
||||
return this.processArray(data, requestHref);
|
||||
} else if (isObjectLevel(data)) {
|
||||
data = this.fixBadEPersonRestResponse(data);
|
||||
const object = this.deserialize(data);
|
||||
if (isNotEmpty(data._embedded)) {
|
||||
Object
|
||||
@@ -53,6 +56,7 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.cache(object, requestHref);
|
||||
return object;
|
||||
}
|
||||
@@ -145,4 +149,23 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
return obj[keys[0]];
|
||||
}
|
||||
|
||||
// TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
|
||||
// See https://github.com/DSpace/dspace-angular/issues/292
|
||||
private fixBadEPersonRestResponse(obj: any): any {
|
||||
if (obj.type === ResourceType.EPerson) {
|
||||
const groups = obj.groups;
|
||||
const normGroups = [];
|
||||
if (isNotEmpty(groups)) {
|
||||
groups.forEach((group) => {
|
||||
const parts = ['eperson', 'groups', group.uuid];
|
||||
const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString();
|
||||
normGroups.push(href);
|
||||
}
|
||||
)
|
||||
}
|
||||
return Object.assign({}, obj, { groups: normGroups });
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal file
168
src/app/core/data/browse-items-response-parsing-service.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
58
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
58
src/app/core/data/browse-items-response-parsing-service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data from the browse endpoint to a list of DSpaceObjects
|
||||
* @param {RestRequest} request
|
||||
* @param {DSpaceRESTV2Response} data
|
||||
* @returns {RestResponse}
|
||||
*/
|
||||
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 }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -9,7 +9,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';
|
||||
@@ -52,6 +52,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}`;
|
||||
@@ -98,7 +102,7 @@ describe('ComColDataService', () => {
|
||||
);
|
||||
}
|
||||
|
||||
describe('getScopedEndpoint', () => {
|
||||
describe('getBrowseEndpoint', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
@@ -112,7 +116,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);
|
||||
@@ -128,13 +132,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);
|
||||
@@ -151,7 +155,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);
|
||||
|
@@ -7,7 +7,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';
|
||||
import { DSOSuccessResponse } from '../cache/response-cache.models';
|
||||
@@ -27,17 +27,18 @@ 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().pipe(
|
||||
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)),
|
||||
first((href: string) => isNotEmpty(href)),
|
||||
tap((href: string) => {
|
||||
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID);
|
||||
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
||||
.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, options.scopeID);
|
||||
this.requestService.configure(request);
|
||||
}),);
|
||||
});
|
||||
|
||||
// return scopeCommunityHrefObs.pipe(
|
||||
// mergeMap((href: string) => this.responseCache.get(href)),
|
||||
@@ -61,16 +62,15 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
|
||||
map((entry: ResponseCacheEntry) => entry.response));
|
||||
const errorResponses = responses.pipe(
|
||||
filter((response) => !response.isSuccessful),
|
||||
mergeMap(() => observableThrowError(new Error(`The Community with scope ${scopeID} couldn't be retrieved`)))
|
||||
mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
|
||||
);
|
||||
const successResponses = responses.pipe(
|
||||
filter((response) => response.isSuccessful),
|
||||
mergeMap(() => this.objectCache.getByUUID(scopeID)),
|
||||
mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
|
||||
map((nc: NormalizedCommunity) => nc._links[this.linkPath]),
|
||||
filter((href) => isNotEmpty(href))
|
||||
);
|
||||
|
||||
|
||||
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged());
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
|
||||
|
||||
const LINK_NAME = 'test'
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
class NormalizedTestObject extends NormalizedObject {
|
||||
@@ -28,10 +28,9 @@ class TestService extends DataService<NormalizedTestObject, any> {
|
||||
super();
|
||||
}
|
||||
|
||||
public getScopedEndpoint(scope: string): Observable<string> {
|
||||
throw new Error('getScopedEndpoint is abstract in DataService');
|
||||
public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
|
||||
return Observable.of(endpoint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
describe('DataService', () => {
|
||||
@@ -42,7 +41,6 @@ describe('DataService', () => {
|
||||
const halService = {} as HALEndpointService;
|
||||
const rdbService = {} as RemoteDataBuildService;
|
||||
const store = {} as Store<CoreState>;
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
function initTestService(): TestService {
|
||||
return new TestService(
|
||||
@@ -50,7 +48,7 @@ describe('DataService', () => {
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
LINK_NAME,
|
||||
endpoint,
|
||||
halService
|
||||
);
|
||||
}
|
||||
@@ -62,25 +60,17 @@ describe('DataService', () => {
|
||||
it('should return an observable with the endpoint', () => {
|
||||
options = {};
|
||||
|
||||
(service as any).getFindAllHref(endpoint).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(endpoint);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// getScopedEndpoint is not implemented in abstract DataService
|
||||
it('should throw error if scopeID provided in options', () => {
|
||||
options = { scopeID: 'somevalue' };
|
||||
|
||||
expect(() => { (service as any).getFindAllHref(endpoint, options) })
|
||||
.toThrowError('getScopedEndpoint is abstract in DataService');
|
||||
});
|
||||
|
||||
it('should include page in href if currentPage provided in options', () => {
|
||||
options = { currentPage: 2 };
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -89,7 +79,7 @@ describe('DataService', () => {
|
||||
options = { elementsPerPage: 5 };
|
||||
const expected = `${endpoint}?size=${options.elementsPerPage}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -99,7 +89,7 @@ describe('DataService', () => {
|
||||
options = { sort: sortOptions};
|
||||
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -108,7 +98,7 @@ describe('DataService', () => {
|
||||
options = { startsWith: 'ab' };
|
||||
const expected = `${endpoint}?startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -124,7 +114,7 @@ describe('DataService', () => {
|
||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||
|
||||
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
})
|
||||
|
@@ -1,7 +1,5 @@
|
||||
|
||||
import { filter, take, first } from 'rxjs/operators';
|
||||
import {of as observableOf, Observable } from 'rxjs';
|
||||
|
||||
import {mergeMap, first, take, distinctUntilChanged, map, filter} from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -14,6 +12,8 @@ import { RemoteData } from './remote-data';
|
||||
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
import map = promise.map;
|
||||
|
||||
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
|
||||
protected abstract responseCache: ResponseCacheService;
|
||||
@@ -23,17 +23,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).pipe(distinctUntilChanged());
|
||||
} else {
|
||||
result = observableOf(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 */
|
||||
@@ -60,12 +56,11 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
||||
}
|
||||
|
||||
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(filter((href: string) => isNotEmpty(href)),
|
||||
mergeMap((endpoint: string) => this.getFindAllHref(endpoint, options)),);
|
||||
const hrefObs = this.getFindAllHref(options);
|
||||
|
||||
hrefObs.pipe(
|
||||
filter((href: string) => hasValue(href)),
|
||||
take(1),)
|
||||
take(1))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
|
||||
this.requestService.configure(request);
|
||||
|
@@ -10,6 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { DataService } from './data.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestService } from './request.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
|
||||
@@ -24,8 +25,8 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
||||
super();
|
||||
}
|
||||
|
||||
getScopedEndpoint(scope: string): Observable<string> {
|
||||
return undefined;
|
||||
getBrowseEndpoint(options: FindAllOptions): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
getFindByIDHref(endpoint, resourceID): string {
|
||||
|
@@ -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,6 +21,14 @@ describe('ItemDataService', () => {
|
||||
const halEndpointService = {} as HALEndpointService;
|
||||
|
||||
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||
const options = Object.assign(new FindAllOptions(), {
|
||||
scopeID: scopeID,
|
||||
sort: {
|
||||
field: '',
|
||||
direction: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const browsesEndpoint = 'https://rest.api/discover/browses';
|
||||
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
|
||||
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
||||
@@ -46,16 +55,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 +76,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);
|
||||
|
@@ -1,11 +1,7 @@
|
||||
|
||||
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedItem } from '../cache/models/normalized-item.model';
|
||||
@@ -17,6 +13,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> {
|
||||
@@ -32,15 +29,21 @@ 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).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()),
|
||||
distinctUntilChanged(),);
|
||||
/**
|
||||
* Get the endpoint for browsing items
|
||||
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
||||
* @param {FindAllOptions} options
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
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}`).toString())
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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 */
|
||||
|
||||
@@ -184,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);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { Group } from './group.model';
|
||||
|
||||
export class Eperson extends DSpaceObject {
|
||||
export class EPerson extends DSpaceObject {
|
||||
|
||||
public handle: string;
|
||||
|
||||
|
@@ -2,13 +2,13 @@ import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||
import { Eperson } from './eperson.model';
|
||||
import { EPerson } from './eperson.model';
|
||||
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
@mapsTo(Eperson)
|
||||
@mapsTo(EPerson)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedEpersonModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||
export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||
|
||||
@autoserialize
|
||||
public handle: string;
|
@@ -2,13 +2,12 @@ import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||
import { Eperson } from './eperson.model';
|
||||
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||
import { Group } from './group.model';
|
||||
|
||||
@mapsTo(Group)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedGroupModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||
export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||
|
||||
@autoserialize
|
||||
public handle: string;
|
@@ -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;
|
||||
|
@@ -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';
|
||||
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[];
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, first, 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';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { SearchResult } from '../../+search-page/search-result.model';
|
||||
@@ -62,3 +63,24 @@ export const toDSpaceObjectListRD = () =>
|
||||
return Object.assign(rd, {payload: payload});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the browse links from a definition by ID given an array of all definitions
|
||||
* @param {string} definitionID
|
||||
* @returns {(source: Observable<RemoteData<BrowseDefinition[]>>) => Observable<any>}
|
||||
*/
|
||||
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}'`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -6,7 +6,7 @@ export enum ResourceType {
|
||||
Item = 'item',
|
||||
Collection = 'collection',
|
||||
Community = 'community',
|
||||
Eperson = 'eperson',
|
||||
EPerson = 'eperson',
|
||||
Group = 'group',
|
||||
ResourcePolicy = 'resourcePolicy'
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ import { RouterStub } from '../shared/testing/router-stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import * as ngrx from '@ngrx/store';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
let comp: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
let store: Store<HeaderState>;
|
||||
@@ -35,11 +37,12 @@ describe('HeaderComponent', () => {
|
||||
NgbCollapseModule.forRoot(),
|
||||
NoopAnimationsModule,
|
||||
ReactiveFormsModule],
|
||||
declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent],
|
||||
declarations: [HeaderComponent],
|
||||
providers: [
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
]
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents(); // compile template and css
|
||||
}));
|
||||
|
@@ -5,7 +5,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
import { authReducer, AuthState } from '../../core/auth/auth.reducer';
|
||||
import { EpersonMock } from '../testing/eperson-mock';
|
||||
import { EPersonMock } from '../testing/eperson-mock';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AuthNavMenuComponent } from './auth-nav-menu.component';
|
||||
@@ -13,6 +13,7 @@ import { HostWindowServiceStub } from '../testing/host-window-service-stub';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
describe('AuthNavMenuComponent', () => {
|
||||
|
||||
@@ -31,7 +32,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: new AuthTokenInfo('test_token'),
|
||||
user: EpersonMock
|
||||
user: EPersonMock
|
||||
};
|
||||
let routerState = {
|
||||
url: '/home'
|
||||
@@ -53,6 +54,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{provide: HostWindowService, useValue: window},
|
||||
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
@@ -222,6 +224,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{provide: HostWindowService, useValue: window},
|
||||
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
|
@@ -14,8 +14,9 @@ import {
|
||||
isAuthenticated,
|
||||
isAuthenticationLoading
|
||||
} from '../../core/auth/selectors';
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-auth-nav-menu',
|
||||
@@ -40,10 +41,14 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
|
||||
public showAuth = observableOf(false);
|
||||
|
||||
public user: Observable<Eperson>;
|
||||
public user: Observable<EPerson>;
|
||||
|
||||
public sub: Subscription;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private windowService: HostWindowService) {
|
||||
private windowService: HostWindowService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
@@ -56,12 +61,15 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
|
||||
this.user = this.store.pipe(select(getAuthenticatedUser));
|
||||
|
||||
this.showAuth = this.store.pipe(
|
||||
select(routerStateSelector),
|
||||
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
|
||||
map((router: RouterReducerState) => {
|
||||
return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
|
||||
})
|
||||
);
|
||||
this.showAuth = this.store.select(routerStateSelector)
|
||||
.filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state))
|
||||
.map((router: RouterReducerState) => {
|
||||
const url = router.state.url;
|
||||
const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
|
||||
if (show) {
|
||||
this.authService.setRedirectUrl(url);
|
||||
}
|
||||
return show;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
12
src/app/shared/browse-by/browse-by.component.html
Normal file
12
src/app/shared/browse-by/browse-by.component.html
Normal 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>
|
0
src/app/shared/browse-by/browse-by.component.scss
Normal file
0
src/app/shared/browse-by/browse-by.component.scss
Normal file
44
src/app/shared/browse-by/browse-by.component.spec.ts
Normal file
44
src/app/shared/browse-by/browse-by.component.spec.ts
Normal 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();
|
||||
});
|
||||
|
||||
});
|
30
src/app/shared/browse-by/browse-by.component.ts
Normal file
30
src/app/shared/browse-by/browse-by.component.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Component to display a browse-by page for any ListableObject
|
||||
*/
|
||||
export class BrowseByComponent {
|
||||
@Input() title: string;
|
||||
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
||||
@Input() paginationConfig: PaginationComponentOptions;
|
||||
@Input() sortConfig: SortOptions;
|
||||
@Input() currentUrl: string;
|
||||
query: string;
|
||||
}
|
@@ -7,8 +7,8 @@ import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
import { LogInComponent } from './log-in.component';
|
||||
import { authReducer } from '../../core/auth/auth.reducer';
|
||||
import { EpersonMock } from '../testing/eperson-mock';
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../testing/eperson-mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../testing/auth-service-stub';
|
||||
@@ -19,7 +19,7 @@ describe('LogInComponent', () => {
|
||||
let component: LogInComponent;
|
||||
let fixture: ComponentFixture<LogInComponent>;
|
||||
let page: Page;
|
||||
let user: Eperson;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
@@ -28,7 +28,7 @@ describe('LogInComponent', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EpersonMock;
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
@@ -5,8 +5,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
import { authReducer } from '../../core/auth/auth.reducer';
|
||||
import { EpersonMock } from '../testing/eperson-mock';
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../testing/eperson-mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AppState } from '../../app.reducer';
|
||||
@@ -18,7 +18,7 @@ describe('LogOutComponent', () => {
|
||||
let component: LogOutComponent;
|
||||
let fixture: ComponentFixture<LogOutComponent>;
|
||||
let page: Page;
|
||||
let user: Eperson;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
@@ -28,7 +28,7 @@ describe('LogOutComponent', () => {
|
||||
const routerStub = new RouterStub();
|
||||
|
||||
beforeEach(() => {
|
||||
user = EpersonMock;
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
@@ -5,6 +5,7 @@ import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { NormalizedObject } from '../../core/cache/models/normalized-object.model';
|
||||
|
||||
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
|
||||
return {
|
||||
@@ -17,7 +18,8 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab
|
||||
payload
|
||||
} as RemoteData<any>)))
|
||||
}
|
||||
}
|
||||
},
|
||||
buildSingle: (href$: string | Observable<string>) => Observable.of(new RemoteData(false, false, true, undefined, {}))
|
||||
} as RemoteDataBuildService;
|
||||
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<ds-object-grid [config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects"
|
||||
[hideGear]="true"
|
||||
[hideGear]="hideGear"
|
||||
*ngIf="getViewMode()===viewModeEnum.Grid">
|
||||
</ds-object-grid>
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
<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>
|
||||
<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="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
</p>
|
||||
<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="hasValue(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">
|
||||
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<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>
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<div class="d-flex flex-row">
|
||||
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
|
||||
{{object.value}}
|
||||
</a>
|
||||
<span class="pr-2"> </span>
|
||||
<span class="badge badge-pill badge-secondary align-self-center">{{object.count}}</span>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
@import '../../../../styles/variables';
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,18 @@
|
||||
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 { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-entry-list-element',
|
||||
styleUrls: ['./browse-entry-list-element.component.scss'],
|
||||
templateUrl: './browse-entry-list-element.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
|
||||
*/
|
||||
@renderElementsFor(BrowseEntry, ViewMode.List)
|
||||
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {}
|
@@ -1,18 +1,24 @@
|
||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||
{{object.findMetadata("dc.title")}}
|
||||
</a>
|
||||
<div>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-list-authors">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
<ds-truncatable [id]="object.id">
|
||||
<a [routerLink]="['/items/' + object.id]" class="lead">
|
||||
{{object.findMetadata("dc.title")}}
|
||||
</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">
|
||||
<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>
|
||||
</span>
|
||||
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
||||
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||
</span>
|
||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
|
||||
</div>
|
||||
</div>
|
||||
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
||||
*ngIf="hasValue(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")}}
|
||||
</div>
|
||||
</ds-truncatable-part>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -76,6 +76,9 @@ 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 { 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';
|
||||
import { DebounceDirective } from './utils/debounce.directive';
|
||||
import { ClickOutsideDirective } from './utils/click-outside.directive';
|
||||
import { EmphasizePipe } from './utils/emphasize.pipe';
|
||||
@@ -155,6 +158,7 @@ const COMPONENTS = [
|
||||
ViewModeSwitchComponent,
|
||||
TruncatableComponent,
|
||||
TruncatablePartComponent,
|
||||
BrowseByComponent,
|
||||
InputSuggestionsComponent
|
||||
];
|
||||
|
||||
@@ -168,6 +172,7 @@ const ENTRY_COMPONENTS = [
|
||||
CollectionGridElementComponent,
|
||||
CommunityGridElementComponent,
|
||||
SearchResultGridElementComponent,
|
||||
BrowseEntryListElementComponent
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
|
@@ -2,12 +2,13 @@ import {of as observableOf, Observable } from 'rxjs';
|
||||
import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { AuthStatus } from '../../core/auth/models/auth-status.model';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { isNotEmpty } from '../empty.util';
|
||||
import { EpersonMock } from './eperson-mock';
|
||||
import { EPersonMock } from './eperson-mock';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
export class AuthRequestServiceStub {
|
||||
protected mockUser: Eperson = EpersonMock;
|
||||
protected mockUser: EPerson = EPersonMock;
|
||||
protected mockTokenInfo = new AuthTokenInfo('test_token');
|
||||
|
||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||
@@ -26,7 +27,7 @@ export class AuthRequestServiceStub {
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
authStatusStub.token = this.mockTokenInfo;
|
||||
authStatusStub.eperson = [this.mockUser];
|
||||
authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
@@ -45,7 +46,7 @@ export class AuthRequestServiceStub {
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
authStatusStub.token = this.mockTokenInfo;
|
||||
authStatusStub.eperson = [this.mockUser];
|
||||
authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
|
@@ -2,8 +2,9 @@
|
||||
import {of as observableOf, Observable } from 'rxjs';
|
||||
import { AuthStatus } from '../../core/auth/models/auth-status.model';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { EpersonMock } from './eperson-mock';
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from './eperson-mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
export class AuthServiceStub {
|
||||
|
||||
@@ -20,7 +21,7 @@ export class AuthServiceStub {
|
||||
authStatus.okay = true;
|
||||
authStatus.authenticated = true;
|
||||
authStatus.token = this.token;
|
||||
authStatus.eperson = [EpersonMock];
|
||||
authStatus.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, EPersonMock));
|
||||
return observableOf(authStatus);
|
||||
} else {
|
||||
console.log('error');
|
||||
@@ -28,9 +29,9 @@ export class AuthServiceStub {
|
||||
}
|
||||
}
|
||||
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
|
||||
if (token.accessToken === 'token_test') {
|
||||
return observableOf(EpersonMock);
|
||||
return Observable.of(EPersonMock);
|
||||
} else {
|
||||
throw(new Error('Message Error test'));
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
|
||||
export const EpersonMock: Eperson = Object.assign(new Eperson(),{
|
||||
export const EPersonMock: EPerson = Object.assign(new EPerson(),{
|
||||
handle: null,
|
||||
groups: [],
|
||||
netid: 'test@test.com',
|
||||
|
Reference in New Issue
Block a user