Merge branch 'master' into performance-optimizations

Conflicts:
	src/app/core/data/request.service.spec.ts
	src/app/core/data/request.service.ts
This commit is contained in:
lotte
2019-03-21 16:00:08 +01:00
53 changed files with 1598 additions and 264 deletions

View File

@@ -668,5 +668,33 @@
}, },
"chips": { "chips": {
"remove": "Remove chip" "remove": "Remove chip"
},
"dso-selector": {
"create": {
"community": {
"head": "New community",
"sub-level": "Create a new community in",
"top-level": "Create a new top-level community"
},
"collection": {
"head": "New collection"
},
"item": {
"head": "New item"
}
},
"edit": {
"community": {
"head": "Edit community"
},
"collection": {
"head": "Edit collection"
},
"item": {
"head": "Edit item"
}
},
"placeholder": "Search for a {{ type }}",
"no-results": "No {{ type }} found"
} }
} }

View File

@@ -12,6 +12,7 @@ import { AuthService } from '../../core/auth/auth.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
describe('AdminSidebarComponent', () => { describe('AdminSidebarComponent', () => {
let comp: AdminSidebarComponent; let comp: AdminSidebarComponent;
@@ -26,7 +27,12 @@ describe('AdminSidebarComponent', () => {
{ provide: Injector, useValue: {} }, { provide: Injector, useValue: {} },
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: AuthService, useClass: AuthServiceStub } { provide: AuthService, useClass: AuthServiceStub },
{
provide: NgbModal, useValue: {
open: () => {/*comment*/}
}
}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(AdminSidebarComponent, { }).overrideComponent(AdminSidebarComponent, {
@@ -96,7 +102,10 @@ describe('AdminSidebarComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(menuService, 'toggleMenu'); spyOn(menuService, 'toggleMenu');
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon')); const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}}); sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
}); });
it('should call toggleMenu on the menuService', () => { it('should call toggleMenu on the menuService', () => {
@@ -108,7 +117,10 @@ describe('AdminSidebarComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(menuService, 'toggleMenu'); spyOn(menuService, 'toggleMenu');
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a')); const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a'));
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}}); sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
}); });
it('should call toggleMenu on the menuService', () => { it('should call toggleMenu on the menuService', () => {
@@ -120,7 +132,10 @@ describe('AdminSidebarComponent', () => {
it('should call expandPreview on the menuService after 100ms', fakeAsync(() => { it('should call expandPreview on the menuService after 100ms', fakeAsync(() => {
spyOn(menuService, 'expandMenuPreview'); spyOn(menuService, 'expandMenuPreview');
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar')); const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
sidebarToggler.triggerEventHandler('mouseenter', {preventDefault: () => {/**/}}); sidebarToggler.triggerEventHandler('mouseenter', {
preventDefault: () => {/**/
}
});
tick(99); tick(99);
expect(menuService.expandMenuPreview).not.toHaveBeenCalled(); expect(menuService.expandMenuPreview).not.toHaveBeenCalled();
tick(1); tick(1);
@@ -132,7 +147,10 @@ describe('AdminSidebarComponent', () => {
it('should call collapseMenuPreview on the menuService after 400ms', fakeAsync(() => { it('should call collapseMenuPreview on the menuService after 400ms', fakeAsync(() => {
spyOn(menuService, 'collapseMenuPreview'); spyOn(menuService, 'collapseMenuPreview');
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar')); const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
sidebarToggler.triggerEventHandler('mouseleave', {preventDefault: () => {/**/}}); sidebarToggler.triggerEventHandler('mouseleave', {
preventDefault: () => {/**/
}
});
tick(399); tick(399);
expect(menuService.collapseMenuPreview).not.toHaveBeenCalled(); expect(menuService.collapseMenuPreview).not.toHaveBeenCalled();
tick(1); tick(1);

View File

@@ -1,6 +1,6 @@
import { Component, Injector, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { slide, slideHorizontal, slideSidebar } from '../../shared/animations/slide'; import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
import { MenuService } from '../../shared/menu/menu.service'; import { MenuService } from '../../shared/menu/menu.service';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state'; import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
@@ -10,6 +10,14 @@ import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { first, map } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
import { combineLatest as combineLatestObservable } from 'rxjs'; import { combineLatest as combineLatestObservable } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -52,7 +60,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
constructor(protected menuService: MenuService, constructor(protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
private variableService: CSSVariableService, private variableService: CSSVariableService,
private authService: AuthService private authService: AuthService,
private modalService: NgbModal
) { ) {
super(menuService, injector); super(menuService, injector);
} }
@@ -104,10 +113,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.new_community', text: 'menu.section.new_community',
link: '/communities/submission' function: () => {
} as LinkMenuItemModel, this.modalService.open(CreateCommunityParentSelectorComponent);
}
} as OnClickMenuItemModel,
}, },
{ {
id: 'new_collection', id: 'new_collection',
@@ -115,10 +126,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.new_collection', text: 'menu.section.new_collection',
link: '/collections/submission' function: () => {
} as LinkMenuItemModel, this.modalService.open(CreateCollectionParentSelectorComponent);
}
} as OnClickMenuItemModel,
}, },
{ {
id: 'new_item', id: 'new_item',
@@ -126,10 +139,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.new_item', text: 'menu.section.new_item',
link: '/items/submission' function: () => {
} as LinkMenuItemModel, this.modalService.open(CreateItemParentSelectorComponent);
}
} as OnClickMenuItemModel,
}, },
{ {
id: 'new_item_version', id: 'new_item_version',
@@ -161,10 +176,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_community', text: 'menu.section.edit_community',
link: '#' function: () => {
} as LinkMenuItemModel, this.modalService.open(EditCommunitySelectorComponent);
}
} as OnClickMenuItemModel,
}, },
{ {
id: 'edit_collection', id: 'edit_collection',
@@ -172,10 +189,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_collection', text: 'menu.section.edit_collection',
link: '#' function: () => {
} as LinkMenuItemModel, this.modalService.open(EditCollectionSelectorComponent);
}
} as OnClickMenuItemModel,
}, },
{ {
id: 'edit_item', id: 'edit_item',
@@ -183,10 +202,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_item', text: 'menu.section.edit_item',
link: '#' function: () => {
} as LinkMenuItemModel, this.modalService.open(EditItemSelectorComponent);
}
} as OnClickMenuItemModel,
}, },
/* Import */ /* Import */
@@ -223,7 +244,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
link: '#' link: '#'
} as LinkMenuItemModel, } as LinkMenuItemModel,
}, },
/* Export */ /* Export */
{ {
id: 'export', id: 'export',

View File

@@ -1,12 +1,14 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module'; import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module'; import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '../shared/shared.module';
@NgModule({ @NgModule({
imports: [ imports: [
AdminRegistriesModule, AdminRegistriesModule,
AdminRoutingModule, AdminRoutingModule,
] SharedModule,
],
}) })
export class AdminModule { export class AdminModule {

View File

@@ -8,17 +8,36 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getCollectionModulePath } from '../app-routing.module';
export const COLLECTION_PARENT_PARAMETER = 'parent';
export function getCollectionPageRoute(collectionId: string) {
return new URLCombiner(getCollectionModulePath(), collectionId).toString();
}
export function getCollectionEditPath(id: string) {
return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString()
}
export function getCollectionCreatePath() {
return new URLCombiner(getCollectionModulePath(), COLLECTION_CREATE_PATH).toString()
}
const COLLECTION_CREATE_PATH = 'create';
const COLLECTION_EDIT_PATH = ':id/edit';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ {
path: 'create', path: COLLECTION_CREATE_PATH,
component: CreateCollectionPageComponent, component: CreateCollectionPageComponent,
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard] canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
}, },
{ {
path: ':id/edit', path: COLLECTION_EDIT_PATH,
pathMatch: 'full', pathMatch: 'full',
component: EditCollectionPageComponent, component: EditCollectionPageComponent,
canActivate: [AuthenticatedGuard], canActivate: [AuthenticatedGuard],

View File

@@ -15,7 +15,6 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
SearchPageModule,
CollectionPageRoutingModule CollectionPageRoutingModule
], ],
declarations: [ declarations: [

View File

@@ -8,17 +8,36 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component'; import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getCommunityModulePath } from '../app-routing.module';
export const COMMUNITY_PARENT_PARAMETER = 'parent';
export function getCommunityPageRoute(communityId: string) {
return new URLCombiner(getCommunityModulePath(), communityId).toString();
}
export function getCommunityEditPath(id: string) {
return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString()
}
export function getCommunityCreatePath() {
return new URLCombiner(getCommunityModulePath(), COMMUNITY_CREATE_PATH).toString()
}
const COMMUNITY_CREATE_PATH = 'create';
const COMMUNITY_EDIT_PATH = ':id/edit';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ {
path: 'create', path: COMMUNITY_CREATE_PATH,
component: CreateCommunityPageComponent, component: CreateCommunityPageComponent,
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard] canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
}, },
{ {
path: ':id/edit', path: COMMUNITY_EDIT_PATH,
pathMatch: 'full', pathMatch: 'full',
component: EditCommunityPageComponent, component: EditCommunityPageComponent,
canActivate: [AuthenticatedGuard], canActivate: [AuthenticatedGuard],

View File

@@ -11,7 +11,6 @@ import { CommunitySearchResultListElementComponent } from '../shared/object-list
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component'; import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component' import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { SearchService } from './search-service/search.service';
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component'; import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects'; import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
@@ -49,13 +48,9 @@ const effects = [
SearchResultsComponent, SearchResultsComponent,
SearchSidebarComponent, SearchSidebarComponent,
SearchSettingsComponent, SearchSettingsComponent,
ItemSearchResultListElementComponent,
CollectionSearchResultListElementComponent,
CommunitySearchResultListElementComponent,
ItemSearchResultGridElementComponent, ItemSearchResultGridElementComponent,
CollectionSearchResultGridElementComponent, CollectionSearchResultGridElementComponent,
CommunitySearchResultGridElementComponent, CommunitySearchResultGridElementComponent,
CommunitySearchResultListElementComponent,
SearchFiltersComponent, SearchFiltersComponent,
SearchFilterComponent, SearchFilterComponent,
SearchFacetFilterComponent, SearchFacetFilterComponent,
@@ -71,7 +66,6 @@ const effects = [
SearchFacetRangeOptionComponent SearchFacetRangeOptionComponent
], ],
providers: [ providers: [
SearchService,
SearchSidebarService, SearchSidebarService,
SearchFilterService, SearchFilterService,
SearchConfigurationService SearchConfigurationService

View File

@@ -23,7 +23,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { import {
configureRequest, configureRequest, filterSuccessfulResponses,
getResponseFromEntry, getResponseFromEntry,
getSucceededRemoteData getSucceededRemoteData
} from '../../core/shared/operators'; } from '../../core/shared/operators';
@@ -104,7 +104,7 @@ export class SearchService implements OnDestroy {
// get search results from response cache // get search results from response cache
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe( const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
getResponseFromEntry(), filterSuccessfulResponses(),
map((response: SearchSuccessResponse) => response.results) map((response: SearchSuccessResponse) => response.results)
); );

View File

@@ -8,13 +8,21 @@ const ITEM_MODULE_PATH = 'items';
export function getItemModulePath() { export function getItemModulePath() {
return `/${ITEM_MODULE_PATH}`; return `/${ITEM_MODULE_PATH}`;
} }
const COLLECTION_MODULE_PATH = 'collections';
export function getCollectionModulePath() {
return `/${COLLECTION_MODULE_PATH}`;
}
const COMMUNITY_MODULE_PATH = 'communities';
export function getCommunityModulePath() {
return `/${COMMUNITY_MODULE_PATH}`;
}
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forRoot([ RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' }, { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },

View File

@@ -5,8 +5,14 @@ import {
race as observableRace race as observableRace
} from 'rxjs'; } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { distinctUntilChanged, flatMap, map, startWith, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import {
hasValue,
hasValueOperator,
isEmpty,
isNotEmpty,
isNotUndefined
} from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error'; import { RemoteDataError } from '../../data/remote-data-error';
@@ -50,13 +56,13 @@ export class RemoteDataBuildService {
const payload$ = const payload$ =
observableCombineLatest( observableCombineLatest(
href$.pipe( href$.pipe(
switchMap((href: string) => this.objectCache.getBySelfLink<T>(href)), switchMap((href: string) => this.objectCache.getObjectBySelfLink<T>(href)),
startWith(undefined)), startWith(undefined)),
requestEntry$.pipe( requestEntry$.pipe(
getResourceLinksFromResponse(), getResourceLinksFromResponse(),
switchMap((resourceSelfLinks: string[]) => { switchMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceSelfLinks)) { if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.getBySelfLink<T>(resourceSelfLinks[0]); return this.objectCache.getObjectBySelfLink<T>(resourceSelfLinks[0]);
} else { } else {
return observableOf(undefined); return observableOf(undefined);
} }

View File

@@ -80,7 +80,7 @@ describe('ObjectCacheService', () => {
}); });
// due to the implementation of spyOn above, this subscribe will be synchronous // due to the implementation of spyOn above, this subscribe will be synchronous
service.getBySelfLink(selfLink).pipe(first()).subscribe((o) => { service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => {
expect(o.self).toBe(selfLink); expect(o.self).toBe(selfLink);
// this only works if testObj is an instance of TestClass // this only works if testObj is an instance of TestClass
expect(o instanceof NormalizedItem).toBeTruthy(); expect(o instanceof NormalizedItem).toBeTruthy();
@@ -96,7 +96,7 @@ describe('ObjectCacheService', () => {
}); });
let getObsHasFired = false; let getObsHasFired = false;
const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true); const subscription = service.getObjectBySelfLink(selfLink).subscribe((o) => getObsHasFired = true);
expect(getObsHasFired).toBe(false); expect(getObsHasFired).toBe(false);
subscription.unsubscribe(); subscription.unsubscribe();
}); });
@@ -106,7 +106,7 @@ describe('ObjectCacheService', () => {
it('should return an observable of the array of cached objects with the specified self link and type', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => {
const item = new NormalizedItem(); const item = new NormalizedItem();
item.self = selfLink; item.self = selfLink;
spyOn(service, 'getBySelfLink').and.returnValue(observableOf(item)); spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item));
service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => { service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => {
expect(arr[0].self).toBe(selfLink); expect(arr[0].self).toBe(selfLink);

View File

@@ -68,29 +68,29 @@ export class ObjectCacheService {
/** /**
* Get an observable of the object with the specified UUID * Get an observable of the object with the specified UUID
* *
* The type needs to be specified as well, in order to turn
* the cached plain javascript object in to an instance of
* a class.
*
* e.g. getByUUID('c96588c6-72d3-425d-9d47-fa896255a695', Item)
*
* @param uuid * @param uuid
* The UUID of the object to get * The UUID of the object to get
* @param type * @return Observable<NormalizedObject<T>>
* The type of the object to get * An observable of the requested object in normalized form
* @return Observable<T>
* An observable of the requested object
*/ */
getByUUID<T extends CacheableObject>(uuid: string): Observable<NormalizedObject<T>> { getObjectByUUID<T extends CacheableObject>(uuid: string): Observable<NormalizedObject<T>> {
return this.store.pipe( return this.store.pipe(
select(selfLinkFromUuidSelector(uuid)), select(selfLinkFromUuidSelector(uuid)),
mergeMap((selfLink: string) => this.getBySelfLink(selfLink) mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink)
) )
) )
} }
getBySelfLink<T extends CacheableObject>(selfLink: string): Observable<NormalizedObject<T>> { /**
return this.getEntry(selfLink).pipe( * Get an observable of the object with the specified selfLink
*
* @param selfLink
* The selfLink of the object to get
* @return Observable<NormalizedObject<T>>
* An observable of the requested object in normalized form
*/
getObjectBySelfLink<T extends CacheableObject>(selfLink: string): Observable<NormalizedObject<T>> {
return this.getBySelfLink(selfLink).pipe(
map((entry: ObjectCacheEntry) => { map((entry: ObjectCacheEntry) => {
if (isNotEmpty(entry.patches)) { if (isNotEmpty(entry.patches)) {
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
@@ -108,7 +108,15 @@ export class ObjectCacheService {
); );
} }
private getEntry(selfLink: string): Observable<ObjectCacheEntry> { /**
* Get an observable of the object cache entry with the specified selfLink
*
* @param selfLink
* The selfLink of the object to get
* @return Observable<ObjectCacheEntry>
* An observable of the requested object cache entry
*/
getBySelfLink(selfLink: string): Observable<ObjectCacheEntry> {
return this.store.pipe( return this.store.pipe(
select(entryFromSelfLinkSelector(selfLink)), select(entryFromSelfLinkSelector(selfLink)),
filter((entry) => this.isValid(entry)), filter((entry) => this.isValid(entry)),
@@ -116,12 +124,28 @@ export class ObjectCacheService {
); );
} }
/**
* Get an observable of the request's uuid with the specified selfLink
*
* @param selfLink
* The selfLink of the object to get
* @return Observable<string>
* An observable of the request's uuid
*/
getRequestUUIDBySelfLink(selfLink: string): Observable<string> { getRequestUUIDBySelfLink(selfLink: string): Observable<string> {
return this.getEntry(selfLink).pipe( return this.getBySelfLink(selfLink).pipe(
map((entry: ObjectCacheEntry) => entry.requestUUID), map((entry: ObjectCacheEntry) => entry.requestUUID),
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Get an observable of the request's uuid with the specified uuid
*
* @param uuid
* The uuid of the object to get
* @return Observable<string>
* An observable of the request's uuid
*/
getRequestUUIDByObjectUUID(uuid: string): Observable<string> { getRequestUUIDByObjectUUID(uuid: string): Observable<string> {
return this.store.pipe( return this.store.pipe(
select(selfLinkFromUuidSelector(uuid)), select(selfLinkFromUuidSelector(uuid)),
@@ -150,7 +174,7 @@ export class ObjectCacheService {
*/ */
getList<T extends CacheableObject>(selfLinks: string[]): Observable<Array<NormalizedObject<T>>> { getList<T extends CacheableObject>(selfLinks: string[]): Observable<Array<NormalizedObject<T>>> {
return observableCombineLatest( return observableCombineLatest(
selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink)) selfLinks.map((selfLink: string) => this.getObjectBySelfLink<T>(selfLink))
); );
} }

View File

@@ -47,7 +47,7 @@ describe('ServerSyncBufferEffects', () => {
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ {
provide: ObjectCacheService, useValue: { provide: ObjectCacheService, useValue: {
getBySelfLink: (link) => { getObjectBySelfLink: (link) => {
const object = new DSpaceObject(); const object = new DSpaceObject();
object.self = link; object.self = link;
return observableOf(object); return observableOf(object);

View File

@@ -96,7 +96,7 @@ export class ServerSyncBufferEffects {
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched * @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
*/ */
private applyPatch(href: string): Observable<Action> { private applyPatch(href: string): Observable<Action> {
const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1)); const patchObject = this.objectCache.getObjectBySelfLink(href).pipe(take(1));
return patchObject.pipe( return patchObject.pipe(
map((object) => { map((object) => {

View File

@@ -69,6 +69,7 @@ import { NormalizedObjectBuildService } from './cache/builders/normalized-object
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service';
import { SearchService } from '../+search-page/search-service/search.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -138,6 +139,7 @@ const PROVIDERS = [
CSSVariableService, CSSVariableService,
MenuService, MenuService,
ObjectUpdatesService, ObjectUpdatesService,
SearchService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -94,7 +94,7 @@ describe('ComColDataService', () => {
function initMockObjectCacheService(): ObjectCacheService { function initMockObjectCacheService(): ObjectCacheService {
return jasmine.createSpyObj('objectCache', { return jasmine.createSpyObj('objectCache', {
getByUUID: cold('d-', { getObjectByUUID: cold('d-', {
d: { d: {
_links: { _links: {
[LINK_NAME]: scopedEndpoint [LINK_NAME]: scopedEndpoint
@@ -159,7 +159,7 @@ describe('ComColDataService', () => {
it('should fetch the scope Community from the cache', () => { it('should fetch the scope Community from the cache', () => {
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush(); scheduler.flush();
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID); expect(objectCache.getObjectByUUID).toHaveBeenCalledWith(scopeID);
}); });
it('should return the endpoint to fetch resources within the given scope', () => { it('should return the endpoint to fetch resources within the given scope', () => {

View File

@@ -49,7 +49,7 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
); );
const successResponses = responses.pipe( const successResponses = responses.pipe(
filter((response) => response.isSuccessful), filter((response) => response.isSuccessful),
mergeMap(() => this.objectCache.getByUUID(options.scopeID)), mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)),
map((nc: NormalizedCommunity) => nc._links[linkPath]), map((nc: NormalizedCommunity) => nc._links[linkPath]),
filter((href) => isNotEmpty(href)) filter((href) => isNotEmpty(href))
); );

View File

@@ -67,7 +67,7 @@ describe('DataService', () => {
addPatch: () => { addPatch: () => {
/* empty */ /* empty */
}, },
getBySelfLink: () => { getObjectBySelfLink: () => {
/* empty */ /* empty */
} }
} as any; } as any;
@@ -189,7 +189,7 @@ describe('DataService', () => {
dso2.metadata = [{ key: 'dc.title', value: name2 }]; dso2.metadata = [{ key: 'dc.title', value: name2 }];
spyOn(service, 'findById').and.returnValues(observableOf(dso)); spyOn(service, 'findById').and.returnValues(observableOf(dso));
spyOn(objectCache, 'getBySelfLink').and.returnValues(observableOf(dso)); spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso));
spyOn(objectCache, 'addPatch'); spyOn(objectCache, 'addPatch');
}); });

View File

@@ -139,7 +139,7 @@ export abstract class DataService<T extends CacheableObject> {
* @param {DSpaceObject} object The given object * @param {DSpaceObject} object The given object
*/ */
update(object: T): Observable<RemoteData<T>> { update(object: T): Observable<RemoteData<T>> {
const oldVersion$ = this.objectCache.getBySelfLink(object.self); const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self);
return oldVersion$.pipe(take(1), mergeMap((oldVersion: T) => { return oldVersion$.pipe(take(1), mergeMap((oldVersion: T) => {
const operations = this.comparator.diff(oldVersion, object); const operations = this.comparator.diff(oldVersion, object);
if (isNotEmpty(operations)) { if (isNotEmpty(operations)) {

View File

@@ -1,5 +1,5 @@
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { hasValue } from '../../shared/empty.util'; import { hasNoValue, hasValue } from '../../shared/empty.util';
export class PaginatedList<T> { export class PaginatedList<T> {
@@ -22,6 +22,9 @@ export class PaginatedList<T> {
if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) { if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) {
return this.pageInfo.totalElements; return this.pageInfo.totalElements;
} }
if (hasNoValue(this.page)) {
return 0;
}
return this.page.length; return this.page.length;
} }

View File

@@ -14,6 +14,7 @@ import { BrowseItemsResponseParsingService } from './browse-items-response-parsi
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service'; import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadataschemaParsingService } from './metadataschema-parsing.service';
import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { MetadatafieldParsingService } from './metadatafield-parsing.service';
import { URLCombiner } from '../url-combiner/url-combiner';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -146,11 +147,11 @@ export class FindAllRequest extends GetRequest {
export class EndpointMapRequest extends GetRequest { export class EndpointMapRequest extends GetRequest {
constructor( constructor(
public uuid: string, uuid: string,
public href: string, href: string,
public body?: any body?: any
) { ) {
super(uuid, href, body); super(uuid, new URLCombiner(href, '?endpointMap').toString(), body);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {

View File

@@ -1,5 +1,5 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf, EMPTY } from 'rxjs';
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
@@ -7,6 +7,7 @@ import { CoreState } from '../core.reducers';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import * as ngrx from '@ngrx/store'; import * as ngrx from '@ngrx/store';
import { ActionsSubject, Store } from '@ngrx/store';
import { import {
DeleteRequest, DeleteRequest,
GetRequest, GetRequest,
@@ -18,7 +19,6 @@ import {
RestRequest RestRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { ActionsSubject, Store } from '@ngrx/store';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { MockStore } from '../../shared/testing/mock-store'; import { MockStore } from '../../shared/testing/mock-store';
@@ -42,6 +42,7 @@ describe('RequestService', () => {
const testHeadRequest = new HeadRequest(testUUID, testHref); const testHeadRequest = new HeadRequest(testUUID, testHref);
const testPatchRequest = new PatchRequest(testUUID, testHref); const testPatchRequest = new PatchRequest(testUUID, testHref);
let selectSpy; let selectSpy;
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
@@ -323,6 +324,7 @@ describe('RequestService', () => {
describe('in the ObjectCache', () => { describe('in the ObjectCache', () => {
beforeEach(() => { beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValue(true); (objectCache.hasBySelfLink as any).and.returnValue(true);
spyOn(serviceAsAny, 'hasByHref').and.returnValue(false);
}); });
it('should return true', () => { it('should return true', () => {
@@ -332,63 +334,16 @@ describe('RequestService', () => {
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
}); });
describe('in the responseCache', () => { describe('in the request cache', () => {
beforeEach(() => { beforeEach(() => {
spyOn(serviceAsAny, 'isReusable').and.returnValue(observableOf(true)); (objectCache.hasBySelfLink as any).and.returnValue(false);
spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(undefined)); spyOn(serviceAsAny, 'hasByHref').and.returnValue(true);
}); });
it('should return true', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
describe('and it\'s a DSOSuccessResponse', () => { expect(result).toEqual(expected);
beforeEach(() => {
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
response: {
isSuccessful: true,
resourceSelfLinks: [
'https://rest.api/endpoint/selfLink1',
'https://rest.api/endpoint/selfLink2'
]
}
}
));
});
it('should return true if all top level links in the response are cached in the object cache', () => {
(objectCache.hasBySelfLink as any).and.returnValues(false, true, true);
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
it('should return false if not all top level links in the response are cached in the object cache', () => {
(objectCache.hasBySelfLink as any).and.returnValues(false, true, false);
spyOn(service, 'isPending').and.returnValue(false);
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = false;
expect(result).toEqual(expected);
});
});
describe('and it isn\'t a DSOSuccessResponse', () => {
beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValue(false);
(service as any).isReusable.and.returnValue(observableOf(true));
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
response: {
isSuccessful: true
}
}
));
});
it('should return true', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
}); });
}); });
}); });
@@ -462,104 +417,128 @@ describe('RequestService', () => {
}); });
}); });
describe('isReusable', () => { describe('isValid', () => {
describe('when the given UUID is has no value', () => { describe('when the given entry has no value', () => {
let reusable; let valid;
beforeEach(() => { beforeEach(() => {
const uuid = undefined; const entry = undefined;
reusable = serviceAsAny.isReusable(uuid); valid = serviceAsAny.isValid(entry);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); expect(valid).toBe(false);
}) })
}); });
describe('when the given UUID has a value, but no cached entry is found', () => { describe('when the given entry has a value, but the request is not completed', () => {
let reusable; let valid;
const requestEntry = { completed: false };
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByUUID').and.returnValue(observableOf(undefined)); spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry));
const uuid = 'a45bb291-1adb-40d9-b2fc-7ad9080607be'; valid = serviceAsAny.isValid(requestEntry);
reusable = serviceAsAny.isReusable(uuid);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); expect(valid).toBe(false);
}) })
}); });
describe('when the given UUID has a value, a cached entry is found, but it has no response', () => { describe('when the given entry has a value, but the response is not successful', () => {
let reusable; let valid;
const requestEntry = { completed: true, response: { isSuccessful: false } };
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByUUID').and.returnValue(observableOf({ response: undefined })); spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry));
const uuid = '53c9b814-ad8b-4567-9bc1-d9bb6cfba6c8'; valid = serviceAsAny.isValid(requestEntry);
reusable = serviceAsAny.isReusable(uuid);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); expect(valid).toBe(false);
}) })
}); });
describe('when the given UUID has a value, a cached entry is found, but its response was not successful', () => { describe('when the given UUID has a value, its response was successful, but the response is outdated', () => {
let reusable; let valid;
beforeEach(() => {
spyOn(service, 'getByUUID').and.returnValue(observableOf({ response: { isSuccessful: false } }));
const uuid = '694c9b32-7b2e-4788-835b-ef3fc2252e6c';
reusable = serviceAsAny.isReusable(uuid);
});
it('return an observable emitting false', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(false));
})
});
describe('when the given UUID has a value, a cached entry is found, its response was successful, but the response is outdated', () => {
let reusable;
const now = 100000; const now = 100000;
const timeAdded = 99899; const timeAdded = 99899;
const msToLive = 100; const msToLive = 100;
const requestEntry = {
completed: true,
response: {
isSuccessful: true,
timeAdded: timeAdded
},
request: {
responseMsToLive: msToLive,
}
};
beforeEach(() => { beforeEach(() => {
spyOn(Date.prototype, 'getTime').and.returnValue(now); spyOn(Date.prototype, 'getTime').and.returnValue(now);
spyOn(service, 'getByUUID').and.returnValue(observableOf({ spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry));
response: { valid = serviceAsAny.isValid(requestEntry);
isSuccessful: true,
timeAdded: timeAdded
},
request: {
responseMsToLive: msToLive
}
}));
const uuid = 'f9b85788-881c-4994-86b6-bae8dad024d2';
reusable = serviceAsAny.isReusable(uuid);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); expect(valid).toBe(false);
}) })
}); });
describe('when the given UUID has a value, a cached entry is found, its response was successful, and the response is not outdated', () => { describe('when the given UUID has a value, a cached entry is found, its response was successful, and the response is not outdated', () => {
let reusable; let valid;
const now = 100000; const now = 100000;
const timeAdded = 99999; const timeAdded = 99999;
const msToLive = 100; const msToLive = 100;
const requestEntry = {
completed: true,
response: {
isSuccessful: true,
timeAdded: timeAdded
},
request: {
responseMsToLive: msToLive
}
};
beforeEach(() => { beforeEach(() => {
spyOn(Date.prototype, 'getTime').and.returnValue(now); spyOn(Date.prototype, 'getTime').and.returnValue(now);
spyOn(service, 'getByUUID').and.returnValue(observableOf({ spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry));
response: { valid = serviceAsAny.isValid(requestEntry);
isSuccessful: true,
timeAdded: timeAdded
},
request: {
responseMsToLive: msToLive
}
}));
const uuid = 'f9b85788-881c-4994-86b6-bae8dad024d2';
reusable = serviceAsAny.isReusable(uuid);
}); });
it('return an observable emitting true', () => { it('return an observable emitting true', () => {
reusable.subscribe((isReusable) => expect(isReusable).toBe(true)); expect(valid).toBe(true);
}) })
}) })
}) });
describe('hasByHref', () => {
describe('when nothing is returned by getByHref', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(EMPTY);
});
it('hasByHref should return false', () => {
const result = service.hasByHref('');
expect(result).toBe(false);
});
});
describe('when isValid returns false', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
spyOn(service as any, 'isValid').and.returnValue(false);
});
it('hasByHref should return false', () => {
const result = service.hasByHref('');
expect(result).toBe(false);
});
});
describe('when isValid returns true', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
spyOn(service as any, 'isValid').and.returnValue(true);
});
it('hasByHref should return true', () => {
const result = service.hasByHref('');
expect(result).toBe(true);
});
});
});
}); });

View File

@@ -1,38 +1,31 @@
import { merge as observableMerge, Observable, of as observableOf } from 'rxjs'; import { Observable, race as observableRace } from 'rxjs';
import { import { filter, mergeMap, take } from 'rxjs/operators';
filter,
map,
mergeMap,
switchMap,
take,
} from 'rxjs/operators';
import { race as observableRace } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { coreSelector, CoreState } from '../core.reducers';
import { CoreState } from '../core.reducers'; import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer';
import { coreSelector } from '../core.selectors'; import { pathSelector } from '../shared/selectors';
import { import {
IndexName, IndexState, originalRequestUUIDFromRequestUUIDSelector,
MetaIndexState requestIndexSelector,
} from '../index/index.reducer';
import {
originalRequestUUIDFromRequestUUIDSelector, requestIndexSelector,
uuidFromHrefSelector uuidFromHrefSelector
} from '../index/index.selectors'; } from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; import {
RequestConfigureAction,
RequestExecuteAction,
RequestRemoveAction
} from './request.actions';
import { GetRequest, RestRequest } from './request.models'; import { GetRequest, RestRequest } from './request.models';
import { RequestEntry, RequestState } from './request.reducer'; import { RequestEntry, RequestState } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { getResponseFromEntry } from '../shared/operators';
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions'; import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
const requestCacheSelector = createSelector( const requestCacheSelector = createSelector(
@@ -88,6 +81,9 @@ export class RequestService {
return `client/${this.uuidService.generate()}`; return `client/${this.uuidService.generate()}`;
} }
/**
* Check if a request is currently pending
*/
isPending(request: GetRequest): boolean { isPending(request: GetRequest): boolean {
// first check requests that haven't made it to the store yet // first check requests that haven't made it to the store yet
if (this.requestsOnTheirWayToTheStore.includes(request.href)) { if (this.requestsOnTheirWayToTheStore.includes(request.href)) {
@@ -101,10 +97,12 @@ export class RequestService {
.subscribe((re: RequestEntry) => { .subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !re.completed) isPending = (hasValue(re) && !re.completed)
}); });
return isPending; return isPending;
} }
/**
* Retrieve a RequestEntry based on their uuid
*/
getByUUID(uuid: string): Observable<RequestEntry> { getByUUID(uuid: string): Observable<RequestEntry> {
return observableRace( return observableRace(
this.store.pipe(select(entryFromUUIDSelector(uuid))), this.store.pipe(select(entryFromUUIDSelector(uuid))),
@@ -117,6 +115,9 @@ export class RequestService {
); );
} }
/**
* Retrieve a RequestEntry based on their href
*/
getByHref(href: string): Observable<RequestEntry> { getByHref(href: string): Observable<RequestEntry> {
return this.store.pipe( return this.store.pipe(
select(uuidFromHrefSelector(href)), select(uuidFromHrefSelector(href)),
@@ -180,31 +181,11 @@ export class RequestService {
* @param {GetRequest} request The request to check * @param {GetRequest} request The request to check
* @returns {boolean} True if the request is cached or still pending * @returns {boolean} True if the request is cached or still pending
*/ */
private isCachedOrPending(request: GetRequest) { private isCachedOrPending(request: GetRequest): boolean {
let isCached = this.objectCache.hasBySelfLink(request.href); const inReqCache = this.hasByHref(request.href);
if (isCached) { const inObjCache = this.objectCache.hasBySelfLink(request.href);
const responses: Observable<RestResponse> = this.isReusable(request.uuid).pipe( const isCached = inReqCache || inObjCache;
filter((reusable: boolean) => reusable),
switchMap(() => {
return this.getByHref(request.href).pipe(
getResponseFromEntry(),
take(1)
);
}
));
const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error.
const dsoSuccessResponses = responses.pipe(
filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)),
map((response: DSOSuccessResponse) => response.resourceSelfLinks),
map((resourceSelfLinks: string[]) => resourceSelfLinks
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
));
const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true));
observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c);
}
const isPending = this.isPending(request); const isPending = this.isPending(request);
return isCached || isPending; return isCached || isPending;
} }
@@ -227,7 +208,7 @@ export class RequestService {
*/ */
private trackRequestsOnTheirWayToTheStore(request: GetRequest) { private trackRequestsOnTheirWayToTheStore(request: GetRequest) {
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href]; this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href];
this.store.pipe(select(entryFromUUIDSelector(request.href)), this.getByHref(request.href).pipe(
filter((re: RequestEntry) => hasValue(re)), filter((re: RequestEntry) => hasValue(re)),
take(1) take(1)
).subscribe((re: RequestEntry) => { ).subscribe((re: RequestEntry) => {
@@ -244,31 +225,39 @@ export class RequestService {
} }
/** /**
* Check whether a Response should still be cached * Check whether a cached response should still be valid
* *
* @param uuid * @param entry
* the uuid of the entry to check * the entry to check
* @return boolean * @return boolean
* false if the uuid has no value, no entry could be found, the response was nog successful or its time to * false if the uuid has no value, the response was not successful or its time to
* live has exceeded, true otherwise * live was exceeded, true otherwise
*/ */
private isReusable(uuid: string): Observable<boolean> { private isValid(entry: RequestEntry): boolean {
if (hasNoValue(uuid)) { if (hasValue(entry) && entry.completed && entry.response.isSuccessful) {
return observableOf(false); const timeOutdated = entry.response.timeAdded + entry.request.responseMsToLive;
const isOutDated = new Date().getTime() > timeOutdated;
return !isOutDated;
} else { } else {
const requestEntry$ = this.getByUUID(uuid); return false;
return requestEntry$.pipe(
filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)),
map((entry: RequestEntry) => {
if (hasValue(entry) && entry.response.isSuccessful) {
const timeOutdated = entry.response.timeAdded + entry.request.responseMsToLive;
const isOutDated = new Date().getTime() > timeOutdated;
return !isOutDated;
} else {
return false;
}
})
);
} }
} }
/**
* Check whether the request with the specified href is cached
*
* @param href
* The link of the request to check
* @return boolean
* true if the request with the specified href is cached,
* false otherwise
*/
hasByHref(href: string): boolean {
let result = false;
this.getByHref(href).pipe(
take(1)
).subscribe((requestEntry: RequestEntry) => result = this.isValid(requestEntry));
return result;
}
} }

View File

@@ -0,0 +1,20 @@
<div class="form-group w-100 pr-2 pl-2">
<input type="search"
class="form-control"
(click)="$event.stopPropagation();"
placeholder="{{'dso-selector.placeholder' | translate: { type: type.toString().toLowerCase() } }}"
[formControl]="input" dsAutoFocus (keyup.enter)="selectSingleResult()">
</div>
<div class="dropdown-divider"></div>
<div class="scrollable-menu list-group">
<button class="list-group-item list-group-item-action border-0 disabled"
*ngIf="(listEntries$ | async)?.payload.page.length == 0">
{{'dso-selector.no-results' | translate: { type: type.toString().toLowerCase() } }}
</button>
<button *ngFor="let listEntry of (listEntries$ | async)?.payload.page"
class="list-group-item list-group-item-action border-0 list-entry"
title="{{ listEntry.dspaceObject.name }}"
(click)="onSelect.emit(listEntry.dspaceObject)" #listEntryElement>
<ds-wrapper-list-element [object]="listEntry"></ds-wrapper-list-element>
</button>
</div>

View File

@@ -0,0 +1,73 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { DSOSelectorComponent } from './dso-selector.component';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { RemoteData } from '../../../core/data/remote-data';
import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model';
import { Item } from '../../../core/shared/item.model';
import { of as observableOf } from 'rxjs';
import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataValue } from '../../../core/shared/metadata.models';
describe('DSOSelectorComponent', () => {
let component: DSOSelectorComponent;
let fixture: ComponentFixture<DSOSelectorComponent>;
let debugElement: DebugElement;
const currentDSOId = 'test-uuid-ford-sose';
const type = DSpaceObjectType.ITEM;
const searchResult = new ItemSearchResult();
const item = new Item();
item.metadata = {
'dc.title': [Object.assign(new MetadataValue(), {
value: 'Item title',
language: undefined
})]
};
searchResult.dspaceObject = item;
searchResult.hitHighlights = {};
const searchService = jasmine.createSpyObj('searchService', {
search: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(undefined, [searchResult])))
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [DSOSelectorComponent],
providers: [
{ provide: SearchService, useValue: searchService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DSOSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
component.currentDSOId = currentDSOId;
component.type = type;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initially call the search method on the SearchService with the given DSO uuid', () => {
const searchOptions = new PaginatedSearchOptions({
query: currentDSOId,
dsoType: type,
pagination: (component as any).defaultPagination
});
expect(searchService.search).toHaveBeenCalledWith(searchOptions);
});
})
;

View File

@@ -0,0 +1,109 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
QueryList,
ViewChildren
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { debounceTime, startWith, switchMap } from 'rxjs/operators';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { SearchResult } from '../../../+search-page/search-result.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
@Component({
selector: 'ds-dso-selector',
// styleUrls: ['./dso-selector.component.scss'],
templateUrl: './dso-selector.component.html'
})
/**
* Component to render a list of DSO's of which one can be selected
* The user can search the list by using the input field
*/
export class DSOSelectorComponent implements OnInit {
/**
* The initially selected DSO's uuid
*/
@Input() currentDSOId: string;
/**
* The type of DSpace objects this components shows a list of
*/
@Input() type: DSpaceObjectType;
/**
* Emits the selected Object when a user selects it in the list
*/
@Output() onSelect: EventEmitter<DSpaceObject> = new EventEmitter();
/**
* Input form control to query the list
*/
public input: FormControl = new FormControl();
/**
* Default pagination for this feature
*/
private defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any;
/**
* List with search results of DSpace objects for the current query
*/
listEntries$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
/**
* List of element references to all elements
*/
@ViewChildren('listEntryElement') listElements: QueryList<ElementRef>;
/**
* Time to wait before sending a search request to the server when a user types something
*/
debounceTime = 500;
constructor(private searchService: SearchService) {
}
/**
* Fills the listEntries$ variable with search results based on the input field's current value
* The search will always start with the initial currentDSOId value
*/
ngOnInit(): void {
this.input.setValue(this.currentDSOId);
this.listEntries$ = this.input.valueChanges
.pipe(
debounceTime(this.debounceTime),
startWith(this.currentDSOId),
switchMap((query) => {
return this.searchService.search(
new PaginatedSearchOptions({
query: query,
dsoType: this.type,
pagination: this.defaultPagination
})
)
}
)
)
}
/**
* Set focus on the first list element when there is only one result
*/
selectSingleResult(): void {
if (this.listElements.length > 0) {
this.listElements.first.nativeElement.click();
}
}
}

View File

@@ -0,0 +1,72 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import * as collectionRouter from '../../../../+collection-page/collection-page-routing.module';
import { Community } from '../../../../core/shared/community.model';
import { CreateCollectionParentSelectorComponent } from './create-collection-parent-selector.component';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('CreateCollectionParentSelectorComponent', () => {
let component: CreateCollectionParentSelectorComponent;
let fixture: ComponentFixture<CreateCollectionParentSelectorComponent>;
let debugElement: DebugElement;
const community = new Community();
community.uuid = '1234-1234-1234-1234';
community.metadata = {
'dc.title': [
Object.assign(new MetadataValue(), {
value: 'Community title',
language: undefined
})]
};
const router = new RouterStub();
const communityRD = new RemoteData(false, false, true, undefined, community);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const createPath = 'testCreatePath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [CreateCollectionParentSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
spyOnProperty(collectionRouter, 'getCollectionCreatePath').and.callFake(() => {
return () => createPath;
});
fixture = TestBed.createComponent(CreateCollectionParentSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct edit path when navigate is called', () => {
component.navigate(community);
expect(router.navigate).toHaveBeenCalledWith([createPath], { queryParams: { parent: community.uuid } });
});
});

View File

@@ -0,0 +1,46 @@
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { Community } from '../../../../core/shared/community.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import {
COLLECTION_PARENT_PARAMETER,
getCollectionCreatePath
} from '../../../../+collection-page/collection-page-routing.module';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a list of existing communities inside a modal
* Used to choose a community from to create a new collection in
*/
@Component({
selector: 'ds-create-collection-parent-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class CreateCollectionParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.COLLECTION;
selectorType = DSpaceObjectType.COMMUNITY;
action = SelectorActionType.CREATE;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the collection create page
*/
navigate(dso: DSpaceObject) {
const navigationExtras: NavigationExtras = {
queryParams: {
[COLLECTION_PARENT_PARAMETER]: dso.uuid,
}
};
this.router.navigate([getCollectionCreatePath()], navigationExtras);
}
}

View File

@@ -0,0 +1,19 @@
<div>
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<button class="btn btn-outline-primary btn-lg btn-block" (click)="selectObject(undefined)">{{'dso-selector.create.community.top-level' | translate}}</button>
<h3 class="position-relative py-1 my-3 font-weight-normal">
<hr>
<div id="create-community-or-separator" class="text-center position-absolute w-100">
<span class="px-4 bg-white">or</span>
</div>
</h3>
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -0,0 +1,3 @@
#create-community-or-separator {
top: 0;
}

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import * as communityRouter from '../../../../+community-page/community-page-routing.module';
import { Community } from '../../../../core/shared/community.model';
import { CreateCommunityParentSelectorComponent } from './create-community-parent-selector.component';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('CreateCommunityParentSelectorComponent', () => {
let component: CreateCommunityParentSelectorComponent;
let fixture: ComponentFixture<CreateCommunityParentSelectorComponent>;
let debugElement: DebugElement;
const community = new Community();
community.uuid = '1234-1234-1234-1234';
community.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Community title', language: undefined })] };
const router = new RouterStub();
const communityRD = new RemoteData(false, false, true, undefined, community);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const createPath = 'testCreatePath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [CreateCommunityParentSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
spyOnProperty(communityRouter, 'getCommunityCreatePath').and.callFake(() => {
return () => createPath;
});
fixture = TestBed.createComponent(CreateCommunityParentSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct edit path when navigate is called', () => {
component.navigate(community);
expect(router.navigate).toHaveBeenCalledWith([createPath], { queryParams: { parent: community.uuid } });
});
});

View File

@@ -0,0 +1,51 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { hasValue } from '../../../empty.util';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import {
COMMUNITY_PARENT_PARAMETER,
getCommunityCreatePath
} from '../../../../+community-page/community-page-routing.module';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a button - for top communities -
* and a list of parent communities - for sub communities
* inside a modal
* Used to create a new community
*/
@Component({
selector: 'ds-create-community-parent-selector',
styleUrls: ['./create-community-parent-selector.component.scss'],
templateUrl: './create-community-parent-selector.component.html',
})
export class CreateCommunityParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.COMMUNITY;
selectorType = DSpaceObjectType.COMMUNITY;
action = SelectorActionType.CREATE;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the community create page
*/
navigate(dso: DSpaceObject) {
let navigationExtras: NavigationExtras = {};
if (hasValue(dso)) {
navigationExtras = {
queryParams: {
[COMMUNITY_PARENT_PARAMETER]: dso.uuid,
}
};
}
this.router.navigate([getCommunityCreatePath()], navigationExtras);
}
}

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import { Collection } from '../../../../core/shared/collection.model';
import { CreateItemParentSelectorComponent } from './create-item-parent-selector.component';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('CreateItemParentSelectorComponent', () => {
let component: CreateItemParentSelectorComponent;
let fixture: ComponentFixture<CreateItemParentSelectorComponent>;
let debugElement: DebugElement;
const collection = new Collection();
collection.uuid = '1234-1234-1234-1234';
collection.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Collection title', language: undefined })] };
const router = new RouterStub();
const collectionRD = new RemoteData(false, false, true, undefined, collection);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const createPath = 'testCreatePath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [CreateItemParentSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
// spyOnProperty(itemRouter, 'getItemCreatePath').and.callFake(() => {
// return () => createPath;
// });
fixture = TestBed.createComponent(CreateItemParentSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct create path when navigate is called', () => {
/* TODO when there is a specific submission path */
// component.navigate(item);
// expect(router.navigate).toHaveBeenCalledWith([createPath]);
});
});

View File

@@ -0,0 +1,42 @@
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Community } from '../../../../core/shared/community.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { Collection } from '../../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { hasValue } from '../../../empty.util';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a list of existing collections inside a modal
* Used to choose a collection from to create a new item in
*/
@Component({
selector: 'ds-create-item-parent-selector',
// styleUrls: ['./create-item-parent-selector.component.scss'],
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.ITEM;
selectorType = DSpaceObjectType.COLLECTION;
action = SelectorActionType.CREATE;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the item create page
*/
navigate(dso: DSpaceObject) {
// There's no submit path per collection yet...
}
}

View File

@@ -0,0 +1,10 @@
<div>
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -0,0 +1,135 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { Component, DebugElement, NO_ERRORS_SCHEMA, OnInit } from '@angular/core';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { of as observableOf } from 'rxjs';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from './dso-selector-modal-wrapper.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute } from '@angular/router';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { first } from 'rxjs/operators';
import { By } from '@angular/platform-browser';
import { DSOSelectorComponent } from '../dso-selector/dso-selector.component';
import { MockComponent } from 'ng-mocks';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
describe('DSOSelectorModalWrapperComponent', () => {
let component: DSOSelectorModalWrapperComponent;
let fixture: ComponentFixture<DSOSelectorModalWrapperComponent>;
let debugElement: DebugElement;
const item = new Item();
item.uuid = '1234-1234-1234-1234';
item.metadata = {
'dc.title': [Object.assign(new MetadataValue(), {
value: 'Item title',
language: undefined
})]
};
const itemRD = new RemoteData(false, false, true, undefined, item);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [TestComponent, MockComponent(DSOSelectorComponent)],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } }
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
component.ngOnInit();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initially set the DSO to the activated route\'s item/collection/community', () => {
component.dsoRD$
.pipe(first())
.subscribe((a) => {
expect(a).toEqual(itemRD);
})
});
describe('selectObject', () => {
beforeEach(() => {
spyOn(component, 'navigate');
spyOn(component, 'close');
component.selectObject(item)
});
it('should call the close and navigate method on the component with the given DSO', () => {
expect(component.close).toHaveBeenCalled();
expect(component.navigate).toHaveBeenCalledWith(item);
});
});
describe('close', () => {
beforeEach(() => {
component.close();
});
it('should call the close method on the æctive modal', () => {
expect(modalStub.close).toHaveBeenCalled();
});
});
describe('when the onSelect method emits on the child component', () => {
beforeEach(() => {
spyOn(component, 'selectObject');
debugElement.query(By.css('ds-dso-selector')).componentInstance.onSelect.emit(item);
fixture.detectChanges();
});
it('should call the selectObject method on the component with the correct object', () => {
expect(component.selectObject).toHaveBeenCalledWith(item);
});
});
describe('when the click method emits on close button', () => {
beforeEach(() => {
spyOn(component, 'close');
debugElement.query(By.css('button.close')).triggerEventHandler('click', {});
fixture.detectChanges();
});
it('should call the close method on the component', () => {
expect(component.close).toHaveBeenCalled();
});
});
});
@Component({
selector: 'ds-test-cmp',
templateUrl: './dso-selector-modal-wrapper.component.html'
})
class TestComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.ITEM;
selectorType = DSpaceObjectType.ITEM;
action = SelectorActionType.EDIT;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) {
super(activeModal, route);
}
navigate(dso: DSpaceObject) {
/* comment */
}
}

View File

@@ -0,0 +1,73 @@
import { Component, Injectable, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { RemoteData } from '../../../core/data/remote-data';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
export enum SelectorActionType {
CREATE = 'create',
EDIT = 'edit'
}
/**
* Abstract base class that represents a wrapper for modal content used to select a DSpace Object
*/
@Injectable()
export abstract class DSOSelectorModalWrapperComponent implements OnInit {
/**
* The current page's DSO
*/
@Input() dsoRD$: Observable<RemoteData<DSpaceObject>>;
/**
* The type of the DSO that's being edited or created
*/
objectType: DSpaceObjectType;
/**
* The type of DSO that can be selected from this list
*/
selectorType: DSpaceObjectType;
/**
* The type of action to perform
*/
action: SelectorActionType;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) {
}
/**
* Get de current page's DSO based on the selectorType
*/
ngOnInit(): void {
const typeString = this.selectorType.toString().toLowerCase();
this.dsoRD$ = this.route.root.firstChild.firstChild.data.pipe(map((data) => data[typeString]));
}
/**
* Method called when an object has been selected
* @param dso The selected DSpaceObject
*/
selectObject(dso: DSpaceObject) {
this.close();
this.navigate(dso);
}
/**
* Navigate to a page based on the DSpaceObject provided
* @param dso The DSpaceObject which can be used to calculate the page to navigate to
*/
abstract navigate(dso: DSpaceObject);
/**
* Close the modal
*/
close() {
this.activeModal.close();
}
}

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import * as collectionRouter from '../../../../+collection-page/collection-page-routing.module';
import { EditCollectionSelectorComponent } from './edit-collection-selector.component';
import { Collection } from '../../../../core/shared/collection.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('EditCollectionSelectorComponent', () => {
let component: EditCollectionSelectorComponent;
let fixture: ComponentFixture<EditCollectionSelectorComponent>;
let debugElement: DebugElement;
const collection = new Collection();
collection.uuid = '1234-1234-1234-1234';
collection.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Collection title', language: undefined })] };
const router = new RouterStub();
const collectionRD = new RemoteData(false, false, true, undefined, collection);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const editPath = 'testEditPath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [EditCollectionSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
spyOnProperty(collectionRouter, 'getCollectionEditPath').and.callFake(() => {
return () => editPath;
});
fixture = TestBed.createComponent(EditCollectionSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct edit path when navigate is called', () => {
component.navigate(collection);
expect(router.navigate).toHaveBeenCalledWith([editPath]);
});
});

View File

@@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { getCollectionEditPath } from '../../../../+collection-page/collection-page-routing.module';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a list of existing collections inside a modal
* Used to choose a collection from to edit
*/
@Component({
selector: 'ds-edit-collection-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class EditCollectionSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.COLLECTION;
selectorType = DSpaceObjectType.COLLECTION;
action = SelectorActionType.EDIT;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the collection edit page
*/
navigate(dso: DSpaceObject) {
this.router.navigate([getCollectionEditPath(dso.uuid)]);
}
}

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import * as communityRouter from '../../../../+community-page/community-page-routing.module';
import { EditCommunitySelectorComponent } from './edit-community-selector.component';
import { Community } from '../../../../core/shared/community.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('EditCommunitySelectorComponent', () => {
let component: EditCommunitySelectorComponent;
let fixture: ComponentFixture<EditCommunitySelectorComponent>;
let debugElement: DebugElement;
const community = new Community();
community.uuid = '1234-1234-1234-1234';
community.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Community title', language: undefined })] };
const router = new RouterStub();
const communityRD = new RemoteData(false, false, true, undefined, community);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const editPath = 'testEditPath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [EditCommunitySelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
spyOnProperty(communityRouter, 'getCommunityEditPath').and.callFake(() => {
return () => editPath;
});
fixture = TestBed.createComponent(EditCommunitySelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct edit path when navigate is called', () => {
component.navigate(community);
expect(router.navigate).toHaveBeenCalledWith([editPath]);
});
});

View File

@@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { getCommunityEditPath } from '../../../../+community-page/community-page-routing.module';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a list of existing communities inside a modal
* Used to choose a community from to edit
*/
@Component({
selector: 'ds-edit-community-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class EditCommunitySelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.COMMUNITY;
selectorType = DSpaceObjectType.COMMUNITY;
action = SelectorActionType.EDIT;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the community edit page
*/
navigate(dso: DSpaceObject) {
this.router.navigate([getCommunityEditPath(dso.uuid)]);
}
}

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { EditItemSelectorComponent } from './edit-item-selector.component';
import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router-stub';
import * as itemRouter from '../../../../+item-page/item-page-routing.module';
import { MetadataValue } from '../../../../core/shared/metadata.models';
describe('EditItemSelectorComponent', () => {
let component: EditItemSelectorComponent;
let fixture: ComponentFixture<EditItemSelectorComponent>;
let debugElement: DebugElement;
const item = new Item();
item.uuid = '1234-1234-1234-1234';
item.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Item title', language: undefined })] };
const router = new RouterStub();
const itemRD = new RemoteData(false, false, true, undefined, item);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
const editPath = 'testEditPath';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [EditItemSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } }
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
spyOnProperty(itemRouter, 'getItemEditPath').and.callFake(() => {
return () => editPath;
});
fixture = TestBed.createComponent(EditItemSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call navigate on the router with the correct edit path when navigate is called', () => {
component.navigate(item);
expect(router.navigate).toHaveBeenCalledWith([editPath]);
});
});

View File

@@ -0,0 +1,42 @@
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { Community } from '../../../../core/shared/community.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Collection } from '../../../../core/shared/collection.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Item } from '../../../../core/shared/item.model';
import { getItemEditPath } from '../../../../+item-page/item-page-routing.module';
import {
DSOSelectorModalWrapperComponent,
SelectorActionType
} from '../dso-selector-modal-wrapper.component';
/**
* Component to wrap a list of existing items inside a modal
* Used to choose an item from to edit
*/
@Component({
selector: 'ds-edit-item-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class EditItemSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.ITEM;
selectorType = DSpaceObjectType.ITEM;
action = SelectorActionType.EDIT;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
/**
* Navigate to the item edit page
*/
navigate(dso: DSpaceObject) {
this.router.navigate([getItemEditPath(dso.uuid)]);
}
}

View File

@@ -12,7 +12,7 @@ export enum MenuID {
* List of possible MenuItemTypes * List of possible MenuItemTypes
*/ */
export enum MenuItemType { export enum MenuItemType {
TEXT, LINK, ALTMETRIC, SEARCH TEXT, LINK, ALTMETRIC, SEARCH, ONCLICK
} }
/** /**

View File

@@ -0,0 +1,11 @@
import { MenuItemModel } from './menu-item.model';
import { MenuItemType } from '../../initial-menus-state';
/**
* Model representing an OnClick Menu Section
*/
export class OnClickMenuItemModel implements MenuItemModel {
type = MenuItemType.ONCLICK;
text: string;
function: () => {};
}

View File

@@ -0,0 +1 @@
<a class="nav-item nav-link" role="button" (click)="item.function()">{{item.text | translate}}</a>

View File

@@ -0,0 +1,3 @@
a {
cursor: pointer;
}

View File

@@ -0,0 +1,52 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TextMenuItemComponent } from './text-menu-item.component';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { OnClickMenuItemComponent } from './onclick-menu-item.component';
import { OnClickMenuItemModel } from './models/onclick.model';
describe('OnClickMenuItemComponent', () => {
let component: OnClickMenuItemComponent;
let fixture: ComponentFixture<OnClickMenuItemComponent>;
let debugElement: DebugElement;
const text = 'HELLO';
const func = () => {
/* comment */
};
const item = Object.assign(new OnClickMenuItemModel(), { text, function: func });
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [OnClickMenuItemComponent],
providers: [
{ provide: 'itemModelProvider', useValue: item },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
spyOn(item, 'function');
fixture = TestBed.createComponent(OnClickMenuItemComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should contain the correct text', () => {
expect(component).toBeTruthy();
});
it('should contain the text element', () => {
const textContent = debugElement.query(By.css('a')).nativeElement.textContent;
expect(textContent).toEqual(text);
});
it('should contain call the function on the item when clicked', () => {
debugElement.query(By.css('a.nav-link')).triggerEventHandler('click', {});
expect(item.function).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,20 @@
import { Component, Inject } from '@angular/core';
import { MenuItemType } from '../initial-menus-state';
import { rendersMenuItemForType } from '../menu-item.decorator';
import { OnClickMenuItemModel } from './models/onclick.model';
/**
* Component that renders a menu section of type ONCLICK
*/
@Component({
selector: 'ds-onclick-menu-item',
styleUrls: ['./onclick-menu-item.component.scss'],
templateUrl: './onclick-menu-item.component.html'
})
@rendersMenuItemForType(MenuItemType.ONCLICK)
export class OnClickMenuItemComponent {
item: OnClickMenuItemModel;
constructor(@Inject('itemModelProvider') item: OnClickMenuItemModel) {
this.item = item;
}
}

View File

@@ -5,17 +5,20 @@ import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { LinkMenuItemComponent } from './menu-item/link-menu-item.component'; import { LinkMenuItemComponent } from './menu-item/link-menu-item.component';
import { TextMenuItemComponent } from './menu-item/text-menu-item.component'; import { TextMenuItemComponent } from './menu-item/text-menu-item.component';
import { OnClickMenuItemComponent } from './menu-item/onclick-menu-item.component';
const COMPONENTS = [ const COMPONENTS = [
MenuSectionComponent, MenuSectionComponent,
MenuComponent, MenuComponent,
LinkMenuItemComponent, LinkMenuItemComponent,
TextMenuItemComponent TextMenuItemComponent,
OnClickMenuItemComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
LinkMenuItemComponent, LinkMenuItemComponent,
TextMenuItemComponent TextMenuItemComponent,
OnClickMenuItemComponent
]; ];
const MODULES = [ const MODULES = [

View File

@@ -97,6 +97,17 @@ import { AutoFocusDirective } from './utils/auto-focus.directive';
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component'; import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
import { StartsWithDateComponent } from './starts-with/date/starts-with-date.component'; import { StartsWithDateComponent } from './starts-with/date/starts-with-date.component';
import { StartsWithTextComponent } from './starts-with/text/starts-with-text.component'; import { StartsWithTextComponent } from './starts-with/text/starts-with-text.component';
import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.component';
import { CreateCommunityParentSelectorComponent } from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { CreateCollectionParentSelectorComponent } from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { CommunitySearchResultListElementComponent } from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
import { CollectionSearchResultListElementComponent } from './object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
import { ItemSearchResultListElementComponent } from './object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
import { EditItemSelectorComponent } from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { EditCommunitySelectorComponent } from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditCollectionSelectorComponent } from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { DSOSelectorModalWrapperComponent } from './dso-selector/modal-wrappers/dso-selector-modal-wrapper.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -179,6 +190,16 @@ const COMPONENTS = [
TruncatablePartComponent, TruncatablePartComponent,
BrowseByComponent, BrowseByComponent,
InputSuggestionsComponent, InputSuggestionsComponent,
DSOSelectorComponent,
CreateCommunityParentSelectorComponent,
CreateCollectionParentSelectorComponent,
CreateItemParentSelectorComponent,
EditCommunitySelectorComponent,
EditCollectionSelectorComponent,
EditItemSelectorComponent,
CommunitySearchResultListElementComponent,
CollectionSearchResultListElementComponent,
ItemSearchResultListElementComponent,
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -187,13 +208,23 @@ const ENTRY_COMPONENTS = [
CollectionListElementComponent, CollectionListElementComponent,
CommunityListElementComponent, CommunityListElementComponent,
SearchResultListElementComponent, SearchResultListElementComponent,
CommunitySearchResultListElementComponent,
CollectionSearchResultListElementComponent,
ItemSearchResultListElementComponent,
ItemGridElementComponent, ItemGridElementComponent,
CollectionGridElementComponent, CollectionGridElementComponent,
CommunityGridElementComponent, CommunityGridElementComponent,
SearchResultGridElementComponent, SearchResultGridElementComponent,
BrowseEntryListElementComponent, BrowseEntryListElementComponent,
StartsWithDateComponent, StartsWithDateComponent,
StartsWithTextComponent StartsWithTextComponent,
DSOSelectorComponent,
CreateCommunityParentSelectorComponent,
CreateCollectionParentSelectorComponent,
CreateItemParentSelectorComponent,
EditCommunitySelectorComponent,
EditCollectionSelectorComponent,
EditItemSelectorComponent,
]; ];
const PROVIDERS = [ const PROVIDERS = [

View File

@@ -40,4 +40,8 @@ export class SearchServiceStub {
getFilterLabels() { getFilterLabels() {
return observableOf([]); return observableOf([]);
} }
search() {
return observableOf({});
}
} }