diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a578c0d8c1..9e24790fa1 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -25,28 +25,28 @@ import { HostWindowResizeAction } from './shared/host-window.actions'; import { MetadataService } from './core/metadata/metadata.service'; -import { GLOBAL_CONFIG, ENV_CONFIG } from '../config'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; -import { MockTranslateLoader } from './shared/mocks/mock-translate-loader'; -import { MockMetadataService } from './shared/mocks/mock-metadata-service'; +import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock'; +import { MetadataServiceMock } from './shared/mocks/metadata-service.mock'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; -import { AngularticsMock } from './shared/mocks/mock-angulartics.service'; -import { AuthServiceMock } from './shared/mocks/mock-auth.service'; +import { AngularticsMock } from './shared/mocks/angulartics.service.mock'; +import { AuthServiceMock } from './shared/mocks/auth.service.mock'; import { AuthService } from './core/auth/auth.service'; import { MenuService } from './shared/menu/menu.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; -import { CSSVariableServiceStub } from './shared/testing/css-variable-service-stub'; -import { MenuServiceStub } from './shared/testing/menu-service-stub'; +import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub'; +import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { HostWindowService } from './shared/host-window.service'; -import { HostWindowServiceStub } from './shared/testing/host-window-service-stub'; +import { HostWindowServiceStub } from './shared/testing/host-window-service.stub'; import { ActivatedRoute, Router } from '@angular/router'; import { RouteService } from './core/services/route.service'; -import { MockActivatedRoute } from './shared/mocks/mock-active-router'; -import { MockRouter } from './shared/mocks/mock-router'; -import { MockCookieService } from './shared/mocks/mock-cookie.service'; +import { MockActivatedRoute } from './shared/mocks/active-router.mock'; +import { RouterMock } from './shared/mocks/router.mock'; +import { CookieServiceMock } from './shared/mocks/cookie.service.mock'; import { CookieService } from './core/services/cookie.service'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { storeModuleConfig } from './app.reducer'; let comp: AppComponent; let fixture: ComponentFixture; @@ -61,33 +61,32 @@ describe('App component', () => { return TestBed.configureTestingModule({ imports: [ CommonModule, - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), ], declarations: [AppComponent], // declare the test component providers: [ - { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, { provide: NativeWindowService, useValue: new NativeWindowRef() }, - { provide: MetadataService, useValue: new MockMetadataService() }, + { provide: MetadataService, useValue: new MetadataServiceMock() }, { provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() }, { provide: Angulartics2DSpace, useValue: new AngularticsMock() }, { provide: AuthService, useValue: new AuthServiceMock() }, - { provide: Router, useValue: new MockRouter() }, + { provide: Router, useValue: new RouterMock() }, { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: CookieService, useValue: new MockCookieService()}, + { provide: CookieService, useValue: new CookieServiceMock()}, AppComponent, RouteService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) + }); })); // synchronous beforeEach diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4e029af8ca..832a7b642f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,8 +6,6 @@ import { select, Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { GLOBAL_CONFIG, GlobalConfig } from '../config'; - import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/search/host-window.reducer'; @@ -26,6 +24,8 @@ import { Theme } from '../config/theme.inferface'; import { isNotEmpty } from './shared/empty.util'; import { CookieService } from './core/services/cookie.service'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; +import { environment } from '../environments/environment'; +import { models } from './core/core.module'; export const LANG_COOKIE = 'language_cookie'; @@ -44,9 +44,10 @@ export class AppComponent implements OnInit, AfterViewInit { collapsedSidebarWidth: Observable; totalSidebarWidth: Observable; theme: Observable = of({} as any); + notificationOptions = environment.notifications; + models; constructor( - @Inject(GLOBAL_CONFIG) public config: GlobalConfig, @Inject(NativeWindowService) private _window: NativeWindowRef, private translate: TranslateService, private store: Store, @@ -60,11 +61,13 @@ export class AppComponent implements OnInit, AfterViewInit { private windowService: HostWindowService, private cookie: CookieService ) { + /* Use models object so all decorators are actually called */ + this.models = models; // Load all the languages that are defined as active from the config file - translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); + translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); // Load the default language from the config file - translate.setDefaultLang(config.defaultLanguage); + translate.setDefaultLang(environment.defaultLanguage); // Attempt to get the language from a cookie const lang = cookie.get(LANG_COOKIE); @@ -78,7 +81,7 @@ export class AppComponent implements OnInit, AfterViewInit { if (translate.getLangs().includes(translate.getBrowserLang())) { translate.use(translate.getBrowserLang()); } else { - translate.use(config.defaultLanguage); + translate.use(environment.defaultLanguage); } } @@ -87,16 +90,15 @@ export class AppComponent implements OnInit, AfterViewInit { metadata.listenForRouteChange(); - if (config.debug) { - console.info(config); + if (environment.debug) { + console.info(environment); } this.storeCSSVariables(); } ngOnInit() { - - const env: string = this.config.production ? 'Production' : 'Development'; - const color: string = this.config.production ? 'red' : 'green'; + const env: string = environment.production ? 'Production' : 'Development'; + const color: string = environment.production ? 'red' : 'green'; console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); @@ -118,10 +120,18 @@ export class AppComponent implements OnInit, AfterViewInit { } private storeCSSVariables() { - const vars = variables.locals || {}; - Object.keys(vars).forEach((name: string) => { - this.cssService.addCSSVariable(name, vars[name]); - }) + this.cssService.addCSSVariable('xlMin', '1200px'); + this.cssService.addCSSVariable('mdMin', '768px'); + this.cssService.addCSSVariable('lgMin', '576px'); + this.cssService.addCSSVariable('smMin', '0'); + this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28'); + this.cssService.addCSSVariable('sidebarItemsWidth', '250px'); + this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px'); + this.cssService.addCSSVariable('totalSidebarWidth', '303.234px'); + // const vars = variables.locals || {}; + // Object.keys(vars).forEach((name: string) => { + // this.cssService.addCSSVariable(name, vars[name]); + // }) } ngAfterViewInit() { @@ -142,7 +152,7 @@ export class AppComponent implements OnInit, AfterViewInit { } @HostListener('window:resize', ['$event']) - private onResize(event): void { + public onResize(event): void { this.dispatchWindowSize(event.target.innerWidth, event.target.innerHeight); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aaad66adf6..ced019a6f9 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,7 +11,6 @@ import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core'; import { TranslateModule } from '@ngx-translate/core'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; -import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../config'; import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component'; import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; @@ -21,7 +20,7 @@ import { AppComponent } from './app.component'; import { appEffects } from './app.effects'; import { appMetaReducers, debugMetaReducers } from './app.metareducers'; -import { appReducers, AppState } from './app.reducer'; +import { appReducers, AppState, storeModuleConfig } from './app.reducer'; import { CoreModule } from './core/core.module'; import { ClientCookieService } from './core/services/client-cookie.service'; @@ -39,17 +38,15 @@ import { NotificationComponent } from './shared/notifications/notification/notif import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; import { SharedModule } from './shared/shared.module'; import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component'; - -export function getConfig() { - return ENV_CONFIG; -} +import { environment } from '../environments/environment'; +import { BrowserModule } from '@angular/platform-browser'; export function getBase() { - return ENV_CONFIG.ui.nameSpace; + return environment.ui.nameSpace; } -export function getMetaReducers(config: GlobalConfig): Array> { - return config.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; +export function getMetaReducers(): Array> { + return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; } const IMPORTS = [ @@ -63,7 +60,7 @@ const IMPORTS = [ NgbModule, TranslateModule.forRoot(), EffectsModule.forRoot(appEffects), - StoreModule.forRoot(appReducers), + StoreModule.forRoot(appReducers, storeModuleConfig), StoreRouterConnectingModule.forRoot(), ]; @@ -75,15 +72,11 @@ const ENTITY_IMPORTS = [ IMPORTS.push( StoreDevtoolsModule.instrument({ maxAge: 1000, - logOnly: ENV_CONFIG.production, + logOnly: environment.production, }) ); const PROVIDERS = [ - { - provide: GLOBAL_CONFIG, - useFactory: (getConfig) - }, { provide: APP_BASE_HREF, useFactory: (getBase) @@ -91,7 +84,6 @@ const PROVIDERS = [ { provide: USER_PROVIDED_META_REDUCERS, useFactory: getMetaReducers, - deps: [GLOBAL_CONFIG] }, { provide: RouterStateSerializer, @@ -121,6 +113,7 @@ const EXPORTS = [ @NgModule({ imports: [ + BrowserModule.withServerTransition({ appId: 'serverApp' }), ...IMPORTS, ...ENTITY_IMPORTS ], diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index e25ddcd44d..813b8d0f4f 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -85,3 +85,10 @@ export function keySelector(key: string, selector): MemoizedSelector - - + + - - - + + + - - - + + + + diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts index 0ab1fed208..32b5dcf0d0 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.spec.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -5,10 +5,13 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Observable, of as observableOf } from 'rxjs'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../shared/testing/mock-translate-loader'; +import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { VarDirective } from '../shared/utils/var.directive'; import { getTestScheduler } from 'jasmine-marbles'; class TestBreadcrumbsService implements BreadcrumbsService { @@ -64,17 +67,16 @@ describe('BreadcrumbsComponent', () => { beforeEach(async(() => { init(); TestBed.configureTestingModule({ - declarations: [BreadcrumbsComponent], + declarations: [BreadcrumbsComponent, VarDirective], imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } - })], + }), NgbModule], providers: [ - { provide: ActivatedRoute, useValue: route } - - ] + {provide: ActivatedRoute, useValue: route} + ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); @@ -92,14 +94,16 @@ describe('BreadcrumbsComponent', () => { describe('ngOnInit', () => { beforeEach(() => { - spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([])) + spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([])); }); it('should call resolveBreadcrumb on init', () => { router.events = observableOf(new NavigationEnd(0, '', '')); component.ngOnInit(); + fixture.detectChanges(); + expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root); - }) + }); }); describe('resolveBreadcrumbs', () => { diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 2bba3c76b6..af63ec985d 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -1,9 +1,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; -import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util'; +import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; -import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; +import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; /** * Component representing the breadcrumbs of a page @@ -13,22 +13,17 @@ import { combineLatest, Observable, Subscription, of as observableOf } from 'rxj templateUrl: './breadcrumbs.component.html', styleUrls: ['./breadcrumbs.component.scss'] }) -export class BreadcrumbsComponent implements OnInit, OnDestroy { +export class BreadcrumbsComponent implements OnInit { /** - * List of breadcrumbs for this page + * Observable of the list of breadcrumbs for this page */ - breadcrumbs: Breadcrumb[]; + breadcrumbs$: Observable; /** * Whether or not to show breadcrumbs on this page */ showBreadcrumbs: boolean; - /** - * Subscription to unsubscribe from on destroy - */ - subscription: Subscription; - constructor( private route: ActivatedRoute, private router: Router @@ -39,14 +34,11 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { * Sets the breadcrumbs on init for this page */ ngOnInit(): void { - this.subscription = this.router.events.pipe( + this.breadcrumbs$ = this.router.events.pipe( filter((e): e is NavigationEnd => e instanceof NavigationEnd), tap(() => this.reset()), - switchMap(() => this.resolveBreadcrumbs(this.route.root)) - ).subscribe((breadcrumbs) => { - this.breadcrumbs = breadcrumbs; - } - ) + switchMap(() => this.resolveBreadcrumbs(this.route.root)), + ); } /** @@ -81,20 +73,10 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]); } - /** - * Unsubscribe from subscription - */ - ngOnDestroy(): void { - if (hasValue(this.subscription)) { - this.subscription.unsubscribe(); - } - } - /** * Resets the state of the breadcrumbs */ reset() { - this.breadcrumbs = []; this.showBreadcrumbs = true; } } diff --git a/src/app/community-list-page/community-list-datasource.ts b/src/app/community-list-page/community-list-datasource.ts index 3a9d9f2077..b77cbb5246 100644 --- a/src/app/community-list-page/community-list-datasource.ts +++ b/src/app/community-list-page/community-list-datasource.ts @@ -1,3 +1,5 @@ +import { NgZone } from '@angular/core'; +import { FindListOptions } from '../core/data/request.models'; import { CommunityListService, FlatNode } from './community-list-service'; import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; import { BehaviorSubject, Observable, } from 'rxjs'; @@ -14,21 +16,23 @@ export class CommunityListDatasource implements DataSource { private communityList$ = new BehaviorSubject([]); public loading$ = new BehaviorSubject(false); - constructor(private communityListService: CommunityListService) { + constructor(private communityListService: CommunityListService, + private zone: NgZone) { } connect(collectionViewer: CollectionViewer): Observable { return this.communityList$.asObservable(); } - loadCommunities(expandedNodes: FlatNode[]) { + loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) { this.loading$.next(true); - - this.communityListService.loadCommunities(expandedNodes).pipe( - take(1), - finalize(() => this.loading$.next(false)), - ).subscribe((flatNodes: FlatNode[]) => { - this.communityList$.next(flatNodes); + this.zone.runOutsideAngular(() => { + this.communityListService.loadCommunities(findOptions, expandedNodes).pipe( + take(1), + finalize(() => this.zone.run(() => this.loading$.next(false))), + ).subscribe((flatNodes: FlatNode[]) => { + this.zone.run(() => this.communityList$.next(flatNodes)); + }); }); } diff --git a/src/app/community-list-page/community-list-page.component.spec.ts b/src/app/community-list-page/community-list-page.component.spec.ts index 0aa4afce7f..346d414528 100644 --- a/src/app/community-list-page/community-list-page.component.spec.ts +++ b/src/app/community-list-page/community-list-page.component.spec.ts @@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing' import { CommunityListPageComponent } from './community-list-page.component'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; describe('CommunityListPageComponent', () => { @@ -15,7 +15,7 @@ describe('CommunityListPageComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock }, }), ], diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index c3cfef35a0..accd0f23a5 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -1,120 +1,129 @@ -import { of as observableOf } from 'rxjs'; -import { TestBed, inject, async } from '@angular/core/testing'; +import { inject, TestBed } from '@angular/core/testing'; import { Store } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; import { AppState } from '../app.reducer'; -import { MockStore } from '../shared/testing/mock-store'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { PaginatedList } from '../core/data/paginated-list'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { StoreMock } from '../shared/testing/store.mock'; import { CommunityListService, FlatNode, toFlatNode } from './community-list-service'; import { CollectionDataService } from '../core/data/collection-data.service'; -import { PaginatedList } from '../core/data/paginated-list'; -import { PageInfo } from '../core/shared/page-info.model'; import { CommunityDataService } from '../core/data/community-data.service'; -import { - createFailedRemoteDataObject$, - createSuccessfulRemoteDataObject$ -} from '../shared/testing/utils'; import { Community } from '../core/shared/community.model'; import { Collection } from '../core/shared/collection.model'; -import { take } from 'rxjs/operators'; import { FindListOptions } from '../core/data/request.models'; +import { PageInfo } from '../core/shared/page-info.model'; describe('CommunityListService', () => { - let store: MockStore; + let store: StoreMock; const standardElementsPerPage = 2; let collectionDataServiceStub: any; let communityDataServiceStub: any; - const mockSubcommunities1Page1 = [Object.assign(new Community(), { - id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', - uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', - }), - Object.assign(new Community(), { - id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - }) - ]; - const mockCollectionsPage1 = [ - Object.assign(new Collection(), { - id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', - uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed', - name: 'Collection 1' - }), - Object.assign(new Collection(), { - id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', - uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304', - name: 'Collection 2' - }) - ]; - const mockCollectionsPage2 = [ - Object.assign(new Collection(), { - id: 'a5159760-f362-4659-9e81-e3253ad91ede', - uuid: 'a5159760-f362-4659-9e81-e3253ad91ede', - name: 'Collection 3' - }), - Object.assign(new Collection(), { - id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', - uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', - name: 'Collection 4' - }) - ]; - const mockListOfTopCommunitiesPage1 = [ - Object.assign(new Community(), { - id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - }), - Object.assign(new Community(), { - id: '9076bd16-e69a-48d6-9e41-0238cb40d863', - uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), - }), - Object.assign(new Community(), { - id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - }), - ]; - const mockListOfTopCommunitiesPage2 = [ - Object.assign(new Community(), { - id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', - uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - }), - ]; - const mockTopCommunitiesWithChildrenArraysPage1 = [ - { - id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', - subcommunities: mockSubcommunities1Page1, - collections: [], - }, - { - id: '9076bd16-e69a-48d6-9e41-0238cb40d863', - uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: [], - collections: [...mockCollectionsPage1, ...mockCollectionsPage2], - }, - { - id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', - subcommunities: [], - collections: [], - }]; - const mockTopCommunitiesWithChildrenArraysPage2 = [ - { - id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', - uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', - subcommunities: [], - collections: [], - }]; - - const allCommunities = [...mockTopCommunitiesWithChildrenArraysPage1, ...mockTopCommunitiesWithChildrenArraysPage2, ...mockSubcommunities1Page1]; let service: CommunityListService; + let mockSubcommunities1Page1; + let mockCollectionsPage1; + let mockCollectionsPage2; + let mockListOfTopCommunitiesPage1; + let mockListOfTopCommunitiesPage2; + let mockTopCommunitiesWithChildrenArraysPage1; + let mockTopCommunitiesWithChildrenArraysPage2; + let allCommunities; + function init() { + mockSubcommunities1Page1 = [Object.assign(new Community(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + }), + Object.assign(new Community(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + }) + ]; + mockCollectionsPage1 = [ + Object.assign(new Collection(), { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 1' + }), + Object.assign(new Collection(), { + id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + name: 'Collection 2' + }) + ]; + mockCollectionsPage2 = [ + Object.assign(new Collection(), { + id: 'a5159760-f362-4659-9e81-e3253ad91ede', + uuid: 'a5159760-f362-4659-9e81-e3253ad91ede', + name: 'Collection 3' + }), + Object.assign(new Collection(), { + id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', + uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', + name: 'Collection 4' + }) + ]; + mockListOfTopCommunitiesPage1 = [ + Object.assign(new Community(), { + id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)), + collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + }), + Object.assign(new Community(), { + id: '9076bd16-e69a-48d6-9e41-0238cb40d863', + uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', + subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), + }), + Object.assign(new Community(), { + id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', + uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', + subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + }), + ]; + mockListOfTopCommunitiesPage2 = [ + Object.assign(new Community(), { + id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', + uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', + subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + }), + ]; + mockTopCommunitiesWithChildrenArraysPage1 = [ + { + id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + subcommunities: mockSubcommunities1Page1, + collections: [], + }, + { + id: '9076bd16-e69a-48d6-9e41-0238cb40d863', + uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', + subcommunities: [], + collections: [...mockCollectionsPage1, ...mockCollectionsPage2], + }, + { + id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', + uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', + subcommunities: [], + collections: [], + }]; + mockTopCommunitiesWithChildrenArraysPage2 = [ + { + id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', + uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6', + subcommunities: [], + collections: [], + }]; - beforeEach(async(() => { + allCommunities = [...mockTopCommunitiesWithChildrenArraysPage1, ...mockTopCommunitiesWithChildrenArraysPage2, ...mockSubcommunities1Page1]; + + } + beforeEach(() => { + init(); communityDataServiceStub = { findTop(options: FindListOptions = {}) { const allTopComs = [...mockListOfTopCommunitiesPage1, ...mockListOfTopCommunitiesPage2]; @@ -183,12 +192,12 @@ describe('CommunityListService', () => { providers: [CommunityListService, { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub }, - { provide: Store, useValue: MockStore }, + { provide: Store, useValue: StoreMock }, ], }); store = TestBed.get(Store); service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store); - })); + }); it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => { expect(serviceIn).toBeTruthy(); @@ -199,13 +208,18 @@ describe('CommunityListService', () => { let flatNodeList; describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => { let findTopSpy; - beforeEach(() => { + beforeEach((done) => { findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough(); - service.getNextPageTopCommunities(); - const sub = service.loadCommunities(null) - .subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.loadCommunities({ + currentPage: 2, + sort: new SortOptions('dc.title', SortDirection.ASC) + }, null) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => { expect(findTopSpy).toHaveBeenCalled(); @@ -225,10 +239,16 @@ describe('CommunityListService', () => { describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => { let flatNodeList; describe('None expanded: should return list containing only flatnodes of the test top communities', () => { - beforeEach(() => { - const sub = service.loadCommunities(null) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + beforeEach((done) => { + service.loadCommunities({ + currentPage: 1, + sort: new SortOptions('dc.title', SortDirection.ASC) + }, null) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as top community list', () => { expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length); @@ -245,7 +265,7 @@ describe('CommunityListService', () => { }); }); describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => { - beforeEach(() => { + beforeEach((done) => { const expandedNodes = []; mockListOfTopCommunitiesPage1.map((community: Community) => { const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null); @@ -253,9 +273,15 @@ describe('CommunityListService', () => { communityFlatNode.currentCommunityPage = 1; expandedNodes.push(communityFlatNode); }); - const sub = service.loadCommunities(expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.loadCommunities({ + currentPage: 1, + sort: new SortOptions('dc.title', SortDirection.ASC) + }, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as top community list and size of its possible page-limited children', () => { expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length); @@ -270,14 +296,20 @@ describe('CommunityListService', () => { }); }); describe('Just first top comm expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => { - beforeEach(() => { + beforeEach((done) => { const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null); communityFlatNode.currentCollectionPage = 1; communityFlatNode.currentCommunityPage = 1; const expandedNodes = [communityFlatNode]; - const sub = service.loadCommunities(expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.loadCommunities({ + currentPage: 1, + sort: new SortOptions('dc.title', SortDirection.ASC) + }, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as top community list and size of page-limited children of first top community', () => { expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length); @@ -289,14 +321,20 @@ describe('CommunityListService', () => { }); }); describe('Just second top comm expanded, collections at page 2: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => { - beforeEach(() => { + beforeEach((done) => { const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null); communityFlatNode.currentCollectionPage = 2; communityFlatNode.currentCommunityPage = 1; const expandedNodes = [communityFlatNode]; - const sub = service.loadCommunities(expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.loadCommunities({ + currentPage: 1, + sort: new SortOptions('dc.title', SortDirection.ASC) + }, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as top community list and size of page-limited children of second top community', () => { expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length); @@ -316,13 +354,19 @@ describe('CommunityListService', () => { describe('transformListOfCommunities', () => { describe('should transform list of communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => { describe('list of communities with possible children', () => { - const listOfCommunities = mockListOfTopCommunitiesPage1; + let listOfCommunities; + beforeEach(() => { + listOfCommunities = mockListOfTopCommunitiesPage1; + }); let flatNodeList; describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => { - beforeEach(() => { - const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + beforeEach((done) => { + service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as community test list', () => { expect(flatNodeList.length).toEqual(listOfCommunities.length); @@ -339,7 +383,7 @@ describe('CommunityListService', () => { }); }); describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => { - beforeEach(() => { + beforeEach((done) => { const expandedNodes = []; listOfCommunities.map((community: Community) => { const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null); @@ -347,9 +391,12 @@ describe('CommunityListService', () => { communityFlatNode.currentCommunityPage = 1; expandedNodes.push(communityFlatNode); }); - const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be as big as community test list and size of its possible children', () => { expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length); @@ -383,10 +430,13 @@ describe('CommunityListService', () => { }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { - beforeEach(() => { - const sub = service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + beforeEach((done) => { + service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('length of flatnode list should be 1', () => { expect(flatNodeList.length).toEqual(1); @@ -412,10 +462,14 @@ describe('CommunityListService', () => { }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { - beforeAll(() => { - const sub = service.transformCommunity(communityWithSubcoms, 0, null, null) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + beforeAll((done) => { + service.transformCommunity(communityWithSubcoms, 0, null, null) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); + }); it('length of flatnode list should be 1', () => { expect(flatNodeList.length).toEqual(1); @@ -441,14 +495,17 @@ describe('CommunityListService', () => { } }); let flatNodeList; - beforeEach(() => { + beforeEach((done) => { const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null); communityFlatNode.currentCollectionPage = 1; communityFlatNode.currentCommunityPage = 1; const expandedNodes = [communityFlatNode]; - const sub = service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => { expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length); @@ -469,25 +526,29 @@ describe('CommunityListService', () => { }); describe('topcommunity with collections, expanded, on second page of collections', () => { describe('should return list containing flatnodes of that community, its collections of the first two pages', () => { - const communityWithCollections = Object.assign(new Community(), { - id: '9076bd16-e69a-48d6-9e41-0238cb40d863', - uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), - collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), - metadata: { - 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 1' }] - } - }); + let communityWithCollections; let flatNodeList; - beforeEach(() => { + beforeEach((done) => { + communityWithCollections = Object.assign(new Community(), { + id: '9076bd16-e69a-48d6-9e41-0238cb40d863', + uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', + subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), + metadata: { + 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], + 'dc.title': [{ language: 'en_US', value: 'Community 1' }] + } + }); const communityFlatNode = toFlatNode(communityWithCollections, observableOf(true), 0, true, null); communityFlatNode.currentCollectionPage = 2; communityFlatNode.currentCommunityPage = 1; const expandedNodes = [communityFlatNode]; - const sub = service.transformCommunity(communityWithCollections, 0, null, expandedNodes) - .pipe(take(1)).subscribe((value) => flatNodeList = value); - sub.unsubscribe(); + service.transformCommunity(communityWithCollections, 0, null, expandedNodes) + .pipe(take(1)) + .subscribe((value) => { + flatNodeList = value; + done(); + }); }); it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => { expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length); @@ -518,7 +579,7 @@ describe('CommunityListService', () => { describe('getIsExpandable', () => { describe('should return true', () => { - it('if community has subcommunities', () => { + it('if community has subcommunities', (done) => { const communityWithSubcoms = Object.assign(new Community(), { id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', @@ -531,9 +592,10 @@ describe('CommunityListService', () => { }); service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => { expect(result).toEqual(true); + done(); }); }); - it('if community has collections', () => { + it('if community has collections', (done) => { const communityWithCollections = Object.assign(new Community(), { id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', @@ -546,11 +608,12 @@ describe('CommunityListService', () => { }); service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => { expect(result).toEqual(true); + done(); }); }); }); describe('should return false', () => { - it('if community has neither subcommunities nor collections', () => { + it('if community has neither subcommunities nor collections', (done) => { const communityWithNoSubcomsOrColls = Object.assign(new Community(), { id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', @@ -563,6 +626,7 @@ describe('CommunityListService', () => { }); service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => { expect(result).toEqual(false); + done(); }); }); }); diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index be04887e71..a5c3506e3d 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -4,11 +4,12 @@ import { combineLatest as observableCombineLatest } from 'rxjs/internal/observab import { Observable, of as observableOf } from 'rxjs'; import { AppState } from '../app.reducer'; import { CommunityDataService } from '../core/data/community-data.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { catchError, filter, map, switchMap, take } from 'rxjs/operators'; +import { FindListOptions } from '../core/data/request.models'; +import { map, flatMap } from 'rxjs/operators'; import { Community } from '../core/shared/community.model'; import { Collection } from '../core/shared/collection.model'; +import { getSucceededRemoteData } from '../core/shared/operators'; +import { PageInfo } from '../core/shared/page-info.model'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { RemoteData } from '../core/data/remote-data'; import { PaginatedList } from '../core/data/paginated-list'; @@ -46,8 +47,7 @@ export class ShowMoreFlatNode { // Helper method to combine an flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Array>): Observable => observableCombineLatest(...obsList).pipe( - map((matrix: FlatNode[][]) => - matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList])) + map((matrix: any[][]) => [].concat(...matrix)) ); /** @@ -99,6 +99,8 @@ const communityListStateSelector = (state: AppState) => state.communityList; const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes); const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode); +export const MAX_COMCOLS_PER_PAGE = 50; + /** * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource * and connection to the store to retrieve and save the state of the community list @@ -107,26 +109,8 @@ const loadingNodeSelector = createSelector(communityListStateSelector, (communit @Injectable() export class CommunityListService { - // page-limited list of top-level communities - payloads$: Array>>; - - topCommunitiesConfig: PaginationComponentOptions; - topCommunitiesSortConfig: SortOptions; - - maxSubCommunitiesPerPage: number; - maxCollectionsPerPage: number; - constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService, private store: Store) { - this.topCommunitiesConfig = new PaginationComponentOptions(); - this.topCommunitiesConfig.id = 'top-level-pagination'; - this.topCommunitiesConfig.pageSize = 10; - this.topCommunitiesConfig.currentPage = 1; - this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.initTopCommunityList(); - - this.maxSubCommunitiesPerPage = 3; - this.maxCollectionsPerPage = 3; } saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void { @@ -141,57 +125,46 @@ export class CommunityListService { return this.store.select(loadingNodeSelector); } - /** - * Increases the payload so it contains the next page of top level communities - */ - getNextPageTopCommunities(): void { - this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1; - this.payloads$ = [...this.payloads$, this.communityDataService.findTop({ - currentPage: this.topCommunitiesConfig.currentPage, - elementsPerPage: this.topCommunitiesConfig.pageSize, - sort: { - field: this.topCommunitiesSortConfig.field, - direction: this.topCommunitiesSortConfig.direction - } - }).pipe( - take(1), - map((results) => results.payload), - )]; - } - /** * Gets all top communities, limited by page, and transforms this in a list of flatNodes. * @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need * not be added to the list */ - loadCommunities(expandedNodes: FlatNode[]): Observable { - const res = this.payloads$.map((payload) => { - return payload.pipe( - take(1), - switchMap((result: PaginatedList) => { - return this.transformListOfCommunities(result, 0, null, expandedNodes); - }), - catchError(() => observableOf([])), - ); - }); - return combineAndFlatten(res); + loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]): Observable { + const currentPage = findOptions.currentPage; + const topCommunities = []; + for (let i = 1; i <= currentPage; i++) { + const pagination: FindListOptions = Object.assign({}, findOptions, { currentPage: i }); + topCommunities.push(this.getTopCommunities(pagination)); + } + const topComs$ = observableCombineLatest(...topCommunities).pipe( + map((coms: Array>) => { + const newPages: Community[][] = coms.map((unit: PaginatedList) => unit.page); + const newPage: Community[] = [].concat(...newPages); + let newPageInfo = new PageInfo(); + if (coms && coms.length > 0) { + newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage }) + } + return new PaginatedList(newPageInfo, newPage); + }) + ); + return topComs$.pipe(flatMap((topComs: PaginatedList) => this.transformListOfCommunities(topComs, 0, null, expandedNodes))); }; /** * Puts the initial top level communities in a list to be called upon */ - private initTopCommunityList(): void { - this.payloads$ = [this.communityDataService.findTop({ - currentPage: this.topCommunitiesConfig.currentPage, - elementsPerPage: this.topCommunitiesConfig.pageSize, + private getTopCommunities(options: FindListOptions): Observable> { + return this.communityDataService.findTop({ + currentPage: options.currentPage, + elementsPerPage: MAX_COMCOLS_PER_PAGE, sort: { - field: this.topCommunitiesSortConfig.field, - direction: this.topCommunitiesSortConfig.direction + field: options.sort.field, + direction: options.sort.direction } }).pipe( - take(1), map((results) => results.payload), - )]; + ); } /** @@ -206,16 +179,15 @@ export class CommunityListService { parent: FlatNode, expandedNodes: FlatNode[]): Observable { if (isNotEmpty(listOfPaginatedCommunities.page)) { - let currentPage = this.topCommunitiesConfig.currentPage; + let currentPage = listOfPaginatedCommunities.currentPage; if (isNotEmpty(parent)) { currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage; } - const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage)); let obsList = listOfPaginatedCommunities.page .map((community: Community) => { return this.transformCommunity(community, level, parent, expandedNodes) }); - if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) { + if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) { obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])]; } @@ -252,13 +224,12 @@ export class CommunityListService { let subcoms = []; for (let i = 1; i <= currentCommunityPage; i++) { const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { - elementsPerPage: this.maxSubCommunitiesPerPage, + elementsPerPage: MAX_COMCOLS_PER_PAGE, currentPage: i }) .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), - switchMap((rd: RemoteData>) => + getSucceededRemoteData(), + flatMap((rd: RemoteData>) => this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes)) ); @@ -271,16 +242,15 @@ export class CommunityListService { let collections = []; for (let i = 1; i <= currentCollectionPage; i++) { const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, { - elementsPerPage: this.maxCollectionsPerPage, + elementsPerPage: MAX_COMCOLS_PER_PAGE, currentPage: i }) .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), + getSucceededRemoteData(), map((rd: RemoteData>) => { let nodes = rd.payload.page .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); - if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) { + if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) { nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; } return nodes; @@ -305,21 +275,18 @@ export class CommunityListService { let hasColls$: Observable; hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 }) .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), + getSucceededRemoteData(), map((results) => results.payload.totalElements > 0), ); hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), + getSucceededRemoteData(), map((results) => results.payload.totalElements > 0), ); let hasChildren$: Observable; hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( - take(1), map(([hasSubcoms, hasColls]: [boolean, boolean]) => { if (hasSubcoms || hasColls) { return true; diff --git a/src/app/community-list-page/community-list.reducer.spec.ts b/src/app/community-list-page/community-list.reducer.spec.ts index 63eaaccc03..23999e6166 100644 --- a/src/app/community-list-page/community-list.reducer.spec.ts +++ b/src/app/community-list-page/community-list.reducer.spec.ts @@ -2,7 +2,7 @@ import { of as observableOf } from 'rxjs/internal/observable/of'; import { PaginatedList } from '../core/data/paginated-list'; import { Community } from '../core/shared/community.model'; import { PageInfo } from '../core/shared/page-info.model'; -import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { toFlatNode } from './community-list-service'; import { CommunityListSaveAction } from './community-list.actions'; import { CommunityListReducer } from './community-list.reducer'; diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index c04aadda37..ef9e89ff1b 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -9,11 +9,11 @@ import { } from '../community-list-service'; import { CdkTreeModule } from '@angular/cdk/tree'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { Community } from '../../core/shared/community.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { Collection } from '../../core/shared/collection.model'; @@ -114,15 +114,9 @@ describe('CommunityListComponent', () => { beforeEach(async(() => { communityListServiceStub = { - topPageSize: 2, - topCurrentPage: 1, - collectionPageSize: 2, - subcommunityPageSize: 2, + pageSize: 2, expandedNodes: [], loadingNode: null, - getNextPageTopCommunities() { - this.topCurrentPage++; - }, getLoadingNodeFromStore() { return observableOf(this.loadingNode); }, @@ -133,12 +127,12 @@ describe('CommunityListComponent', () => { this.expandedNodes = expandedNodes; this.loadingNode = loadingNode; }, - loadCommunities(expandedNodes) { + loadCommunities(options, expandedNodes) { let flatnodes; let showMoreTopComNode = false; flatnodes = [...mockTopFlatnodesUnexpanded]; - const currentPage = this.topCurrentPage; - const elementsPerPage = this.topPageSize; + const currentPage = options.currentPage; + const elementsPerPage = this.pageSize; let endPageIndex = (currentPage * elementsPerPage); if (endPageIndex >= flatnodes.length) { endPageIndex = flatnodes.length; @@ -171,14 +165,14 @@ describe('CommunityListComponent', () => { collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)]; }); if (isNotEmpty(subComFlatnodes)) { - const endSubComIndex = this.subcommunityPageSize * expandedParent.currentCommunityPage; + const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage; flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)]; if (subComFlatnodes.length > endSubComIndex) { flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)]; } } if (isNotEmpty(collFlatnodes)) { - const endColIndex = this.collectionPageSize * expandedParent.currentCollectionPage; + const endColIndex = this.pageSize * expandedParent.currentCollectionPage; flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)]; if (collFlatnodes.length > endColIndex) { flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)]; @@ -199,7 +193,7 @@ describe('CommunityListComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock }, }), CdkTreeModule, diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index ddcd49cd1c..be96ff1a0a 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -1,5 +1,7 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; import { take } from 'rxjs/operators'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; import { CommunityListService, FlatNode } from '../community-list-service'; import { CommunityListDatasource } from '../community-list-datasource'; import { FlatTreeControl } from '@angular/cdk/tree'; @@ -27,17 +29,24 @@ export class CommunityListComponent implements OnInit, OnDestroy { dataSource: CommunityListDatasource; - constructor(private communityListService: CommunityListService) { + paginationConfig: FindListOptions; + + constructor(private communityListService: CommunityListService, + private zone: NgZone) { + this.paginationConfig = new FindListOptions(); + this.paginationConfig.elementsPerPage = 2; + this.paginationConfig.currentPage = 1; + this.paginationConfig.sort = new SortOptions('dc.title', SortDirection.ASC); } ngOnInit() { - this.dataSource = new CommunityListDatasource(this.communityListService); + this.dataSource = new CommunityListDatasource(this.communityListService, this.zone); this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => { this.loadingNode = result; }); this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => { this.expandedNodes = [...result]; - this.dataSource.loadCommunities(this.expandedNodes); + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); }); } @@ -74,7 +83,7 @@ export class CommunityListComponent implements OnInit, OnDestroy { node.currentCommunityPage = 1; } } - this.dataSource.loadCommunities(this.expandedNodes); + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } /** @@ -94,10 +103,10 @@ export class CommunityListComponent implements OnInit, OnDestroy { const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id); parentNodeInExpandedNodes.currentCommunityPage++; } - this.dataSource.loadCommunities(this.expandedNodes); + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } else { - this.communityListService.getNextPageTopCommunities(); - this.dataSource.loadCommunities(this.expandedNodes); + this.paginationConfig.currentPage++; + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } } diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index e5c9210769..c54c645094 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,10 +1,8 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models'; import { AuthStatusResponse, ErrorResponse } from '../cache/response.models'; @@ -17,8 +15,7 @@ export class AuthRequestService { protected linkName = 'authn'; protected browseEndpoint = ''; - constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected halService: HALEndpointService, + constructor(protected halService: HALEndpointService, protected requestService: RequestService, private http: HttpClient) { } @@ -69,5 +66,4 @@ export class AuthRequestService { mergeMap((request: GetRequest) => this.fetchRequest(request)), distinctUntilChanged()); } - } diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 3b18d925bf..924c60535d 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -3,12 +3,13 @@ import { async, TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { MockStore } from '../../shared/testing/mock-store'; +import { StoreMock } from '../../shared/testing/store.mock'; import { ObjectCacheService } from '../cache/object-cache.service'; import { AuthStatusResponse } from '../cache/response.models'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthStatus } from './models/auth-status.model'; +import { storeModuleConfig } from '../../app.reducer'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; @@ -21,10 +22,10 @@ describe('AuthResponseParsingService', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), ], providers: [ - { provide: Store, useClass: MockStore } + { provide: Store, useClass: StoreMock } ] }).compileComponents(); })); @@ -35,7 +36,7 @@ describe('AuthResponseParsingService', () => { removeResolvedLinks: {} }); objectCacheService = new ObjectCacheService(store as any, linkServiceStub); - service = new AuthResponseParsingService(EnvConfig, objectCacheService); + service = new AuthResponseParsingService(objectCacheService); }); describe('parse', () => { diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 9ef523ca14..8c77770974 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -3,8 +3,6 @@ import { Inject, Injectable } from '@angular/core'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { AuthStatusResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ResponseParsingService } from '../data/parsing.service'; @@ -16,8 +14,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple protected toCache = true; - constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService) { + constructor(protected objectCache: ObjectCacheService) { super(); } diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 2c2224e878..9237c30db9 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -402,10 +402,10 @@ export class RetrieveAuthenticatedEpersonAction implements Action { */ export class RetrieveAuthenticatedEpersonSuccessAction implements Action { public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS; - payload: EPerson; + payload: string; - constructor(user: EPerson) { - this.payload = user ; + constructor(userId: string) { + this.payload = userId ; } } diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 1f6fa51afd..79fe385c6d 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -1,9 +1,9 @@ -import { TestBed } from '@angular/core/testing'; +import { fakeAsync, flush, TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { cold, hot } from 'jasmine-marbles'; - import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs'; import { AuthEffects } from './auth.effects'; @@ -27,42 +27,55 @@ import { RetrieveAuthMethodsSuccessAction, RetrieveTokenAction } from './auth.actions'; -import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service-stub'; +import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { AuthService } from './auth.service'; -import { AuthState } from './auth.reducer'; -import { EPersonMock } from '../../shared/testing/eperson-mock'; +import { authReducer } from './auth.reducer'; import { AuthStatus } from './models/auth-status.model'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { AppState, storeModuleConfig } from '../../app.reducer'; +import { StoreActionTypes } from '../../store.actions'; +import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; let authServiceStub; - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: observableOf(true) - }); + let initialState; let token; + let store: MockStore; function init() { authServiceStub = new AuthServiceStub(); token = authServiceStub.getToken(); + initialState = { + core: { + auth: { + authenticated: false, + loaded: false, + loading: false, + authMethods: [] + } + } + }; } beforeEach(() => { init(); TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ auth: authReducer }, storeModuleConfig) + ], providers: [ AuthEffects, + provideMockStore({ initialState }), { provide: AuthService, useValue: authServiceStub }, - { provide: Store, useValue: store }, provideMockActions(() => actions), // other providers ], }); authEffects = TestBed.get(AuthEffects); + store = TestBed.get(Store); }); describe('authenticate$', () => { @@ -137,7 +150,8 @@ describe('AuthEffects', () => { describe('authenticatedSuccess$', () => { - it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => { + it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', (done) => { + spyOn((authEffects as any).authService, 'storeToken'); actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: { @@ -150,8 +164,14 @@ describe('AuthEffects', () => { const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) }); + authEffects.authenticatedSuccess$.subscribe(() => { + expect(authServiceStub.storeToken).toHaveBeenCalledWith(token); + }); + expect(authEffects.authenticatedSuccess$).toBeObservable(expected); + done(); }); + }); describe('checkToken$', () => { @@ -234,7 +254,7 @@ describe('AuthEffects', () => { } }); - const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock) }); + const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) }); expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected); }); @@ -361,4 +381,40 @@ describe('AuthEffects', () => { }); }) }); + + describe('clearInvalidTokenOnRehydrate$', () => { + + beforeEach(() => { + store.overrideSelector(isAuthenticated, false); + }); + + describe('when auth loaded is false', () => { + it('should not call removeToken method', (done) => { + store.overrideSelector(isAuthenticatedLoaded, false); + actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); + spyOn(authServiceStub, 'removeToken'); + + authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { + expect(authServiceStub.removeToken).not.toHaveBeenCalled(); + + }); + + done(); + }); + }); + + describe('when auth loaded is true', () => { + it('should call removeToken method', fakeAsync(() => { + store.overrideSelector(isAuthenticatedLoaded, true); + actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); + spyOn(authServiceStub, 'removeToken'); + + authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { + expect(authServiceStub.removeToken).toHaveBeenCalled(); + flush(); + }); + + })); + }); + }); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index d153748fb9..5591ffbe39 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,20 +1,18 @@ -import { Observable, of as observableOf } from 'rxjs'; - -import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; // import @ngrx import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action, select, Store } from '@ngrx/store'; // import services import { AuthService } from './auth.service'; - import { EPerson } from '../eperson/models/eperson.model'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AppState } from '../../app.reducer'; -import { isAuthenticated } from './selectors'; +import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; import { StoreActionTypes } from '../../store.actions'; import { AuthMethod } from './models/auth.method'; // import actions @@ -43,6 +41,7 @@ import { RetrieveAuthMethodsSuccessAction, RetrieveTokenAction } from './auth.actions'; +import { hasValue } from '../../shared/empty.util'; @Injectable() export class AuthEffects { @@ -66,7 +65,6 @@ export class AuthEffects { @Effect() public authenticateSuccess$: Observable = this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), - tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)), map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) ); @@ -83,6 +81,7 @@ export class AuthEffects { @Effect() public authenticatedSuccess$: Observable = this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATED_SUCCESS), + tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref)) ); @@ -97,8 +96,15 @@ export class AuthEffects { public retrieveAuthenticatedEperson$: Observable = this.actions$.pipe( ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON), switchMap((action: RetrieveAuthenticatedEpersonAction) => { - return this.authService.retrieveAuthenticatedUserByHref(action.payload).pipe( - map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user)), + const impersonatedUserID = this.authService.getImpersonateID(); + let user$: Observable; + if (hasValue(impersonatedUserID)) { + user$ = this.authService.retrieveAuthenticatedUserById(impersonatedUserID); + } else { + user$ = this.authService.retrieveAuthenticatedUserByHref(action.payload); + } + return user$.pipe( + map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)), catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error)))); }) ); @@ -179,10 +185,11 @@ export class AuthEffects { public clearInvalidTokenOnRehydrate$: Observable = this.actions$.pipe( ofType(StoreActionTypes.REHYDRATE), switchMap(() => { - return this.store.pipe( - select(isAuthenticated), + const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded)); + const authenticated$ = this.store.pipe(select(isAuthenticated)); + return observableCombineLatest(isLoaded$, authenticated$).pipe( take(1), - filter((authenticated) => !authenticated), + filter(([loaded, authenticated]) => loaded && !authenticated), tap(() => this.authService.removeToken()), tap(() => this.authService.resetAuthenticationError()) ); @@ -193,6 +200,7 @@ export class AuthEffects { .pipe( ofType(AuthActionTypes.LOG_OUT), switchMap(() => { + this.authService.stopImpersonating(); return this.authService.logout().pipe( map((value) => new LogOutSuccessAction()), catchError((error) => observableOf(new LogOutErrorAction(error))) diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 72b0cc2616..be40351795 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -9,9 +9,9 @@ import { of as observableOf } from 'rxjs'; import { AuthInterceptor } from './auth.interceptor'; import { AuthService } from './auth.service'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { RouterStub } from '../../shared/testing/router-stub'; +import { RouterStub } from '../../shared/testing/router.stub'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; -import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RestRequestMethod } from '../data/rest-request-method'; describe(`AuthInterceptor`, () => { @@ -48,7 +48,7 @@ describe(`AuthInterceptor`, () => { describe('when has a valid token', () => { - it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => { + it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => { service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => { expect(response).toBeTruthy(); }); @@ -58,8 +58,19 @@ describe(`AuthInterceptor`, () => { const token = httpRequest.request.headers.get('authorization'); expect(token).toBeNull(); }); + it('should add an Authorization header when we’re sending a HTTP request to the\'authn/logout\' endpoint', () => { + service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/logout', 'test').subscribe((response) => { + expect(response).toBeTruthy(); + }); - it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => { + const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/authn/logout`); + + expect(httpRequest.request.headers.has('authorization')); + const token = httpRequest.request.headers.get('authorization'); + expect(token).toBe('Bearer token_test'); + }); + + it('should add an Authorization header when we’re sending a HTTP request to a non-\'authn\' endpoint', () => { service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => { expect(response).toBeTruthy(); }); diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 6d609a4ea3..f4e7aa2fd3 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -18,7 +18,7 @@ import { AppState } from '../../app.reducer'; import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions'; import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; @@ -221,7 +221,7 @@ export class AuthInterceptor implements HttpInterceptor { // Redirect to the login route this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired')); return observableOf(null); - } else if (!this.isAuthRequest(req) && isNotEmpty(token)) { + } else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) { // Intercept a request that is not to the authentication endpoint authService.isTokenExpiring().pipe( filter((isExpiring) => isExpiring)) @@ -235,8 +235,16 @@ export class AuthInterceptor implements HttpInterceptor { }); // Get the auth header from the service. authorization = authService.buildAuthHeader(token); + let newHeaders = req.headers.set('authorization', authorization); + + // When present, add the ID of the EPerson we're impersonating to the headers + const impersonatingID = authService.getImpersonateID(); + if (hasValue(impersonatingID)) { + newHeaders = newHeaders.set('X-On-Behalf-Of', impersonatingID); + } + // Clone the request to add the new header. - newReq = req.clone({ headers: req.headers.set('authorization', authorization) }); + newReq = req.clone({ headers: newHeaders }); } else { newReq = req.clone(); } diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 7a39ef3da4..cf934a7f47 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -26,7 +26,7 @@ import { SetRedirectUrlAction } from './auth.actions'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EPersonMock } from '../../shared/testing/eperson-mock'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; import { AuthStatus } from './models/auth-status.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; @@ -189,7 +189,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutAction(); @@ -206,7 +206,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutSuccessAction(); @@ -219,7 +219,7 @@ describe('authReducer', () => { loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }; expect(newState).toEqual(state); }); @@ -232,7 +232,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutErrorAction(mockError); @@ -244,7 +244,7 @@ describe('authReducer', () => { error: 'Test error message', loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; expect(newState).toEqual(state); }); @@ -258,7 +258,7 @@ describe('authReducer', () => { loading: true, info: undefined }; - const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock); + const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id); const newState = authReducer(initialState, action); state = { authenticated: true, @@ -267,7 +267,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; expect(newState).toEqual(state); }); @@ -301,7 +301,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenAction(newTokenInfo); @@ -313,7 +313,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; expect(newState).toEqual(state); @@ -327,7 +327,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); @@ -340,7 +340,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: false }; expect(newState).toEqual(state); @@ -354,7 +354,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; const action = new RefreshTokenErrorAction(); @@ -367,7 +367,7 @@ describe('authReducer', () => { loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }; expect(newState).toEqual(state); }); @@ -380,7 +380,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; state = { @@ -390,7 +390,7 @@ describe('authReducer', () => { loading: false, error: undefined, info: 'Message', - user: undefined + userId: undefined }; }); diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 19fd162d3f..16990b35a8 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -14,7 +14,6 @@ import { SetRedirectUrlAction } from './auth.actions'; // import models -import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; @@ -49,8 +48,8 @@ export interface AuthState { // true when refreshing token refreshing?: boolean; - // the authenticated user - user?: EPerson; + // the authenticated user's id + userId?: string; // all authentication Methods enabled at the backend authMethods?: AuthMethod[]; @@ -112,7 +111,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut error: undefined, loading: false, info: undefined, - user: (action as RetrieveAuthenticatedEpersonSuccessAction).payload + userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload }); case AuthActionTypes.AUTHENTICATE_ERROR: @@ -144,7 +143,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }); case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED: @@ -155,7 +154,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loaded: false, loading: false, info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload, - user: undefined + userId: undefined }); case AuthActionTypes.REGISTRATION: diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 03759987bf..3b6fae4dd1 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -8,26 +8,27 @@ import { of as observableOf } from 'rxjs'; import { authReducer, AuthState } from './auth.reducer'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { AuthService } from './auth.service'; -import { RouterStub } from '../../shared/testing/router-stub'; -import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { AuthService, IMPERSONATING_COOKIE } from './auth.service'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; + import { CookieService } from '../services/cookie.service'; -import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub'; +import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub'; import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { EPerson } from '../eperson/models/eperson.model'; -import { EPersonMock } from '../../shared/testing/eperson-mock'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; import { AppState } from '../../app.reducer'; import { ClientCookieService } from '../services/client-cookie.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { routeServiceStub } from '../../shared/testing/route-service-stub'; +import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { RouteService } from '../services/route.service'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../data/remote-data'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { authMethodsMock } from '../../shared/testing/auth-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { authMethodsMock } from '../../shared/testing/auth-service.stub'; import { AuthMethod } from './models/auth.method'; describe('AuthService test', () => { @@ -81,7 +82,12 @@ describe('AuthService test', () => { TestBed.configureTestingModule({ imports: [ CommonModule, - StoreModule.forRoot({ authReducer }), + StoreModule.forRoot({ authReducer }, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }), ], declarations: [], providers: [ @@ -174,7 +180,12 @@ describe('AuthService test', () => { init(); TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({ authReducer }) + StoreModule.forRoot({ authReducer }, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }) ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -226,7 +237,12 @@ describe('AuthService test', () => { init(); TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({ authReducer }) + StoreModule.forRoot({ authReducer }, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }) ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -316,5 +332,120 @@ describe('AuthService test', () => { expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/'); }); + + describe('impersonate', () => { + const userId = 'testUserId'; + + beforeEach(() => { + spyOn(authService, 'refreshAfterLogout'); + authService.impersonate(userId); + }); + + it('should impersonate user', () => { + expect(storage.set).toHaveBeenCalledWith(IMPERSONATING_COOKIE, userId); + }); + + it('should call refreshAfterLogout', () => { + expect(authService.refreshAfterLogout).toHaveBeenCalled(); + }); + }); + + describe('stopImpersonating', () => { + beforeEach(() => { + authService.stopImpersonating(); + }); + + it('should impersonate user', () => { + expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + }); + + describe('stopImpersonatingAndRefresh', () => { + beforeEach(() => { + spyOn(authService, 'refreshAfterLogout'); + authService.stopImpersonatingAndRefresh(); + }); + + it('should impersonate user', () => { + expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + + it('should call refreshAfterLogout', () => { + expect(authService.refreshAfterLogout).toHaveBeenCalled(); + }); + }); + + describe('getImpersonateID', () => { + beforeEach(() => { + authService.getImpersonateID(); + }); + + it('should impersonate user', () => { + expect(storage.get).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + }); + + describe('isImpersonating', () => { + const userId = 'testUserId'; + let result: boolean; + + describe('when the cookie doesn\'t contain a value', () => { + beforeEach(() => { + result = authService.isImpersonating(); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when the cookie contains a value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue(userId); + result = authService.isImpersonating(); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + }); + + describe('isImpersonatingUser', () => { + const userId = 'testUserId'; + let result: boolean; + + describe('when the cookie doesn\'t contain a value', () => { + beforeEach(() => { + result = authService.isImpersonatingUser(userId); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when the cookie contains the right value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue(userId); + result = authService.isImpersonatingUser(userId); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + + describe('when the cookie contains the wrong value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue('wrongValue'); + result = authService.isImpersonatingUser(userId); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + }); }); }); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 0f5c06bbc9..588d9e2675 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, filter, map, startWith, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; @@ -14,9 +14,15 @@ import { AuthRequestService } from './auth-request.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; -import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import { CookieService } from '../services/cookie.service'; -import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; +import { + getAuthenticatedUserId, + getAuthenticationToken, + getRedirectUrl, + isAuthenticated, + isTokenRefreshing +} from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { CheckAuthenticationTokenAction, @@ -33,6 +39,7 @@ import { AuthMethod } from './models/auth.method'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; export const REDIRECT_COOKIE = 'dsRedirectUrl'; +export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson'; /** * The auth service. @@ -163,7 +170,7 @@ export class AuthService { } /** - * Returns the authenticated user + * Returns the authenticated user by href * @returns {User} */ public retrieveAuthenticatedUserByHref(userHref: string): Observable { @@ -172,6 +179,29 @@ export class AuthService { ) } + /** + * Returns the authenticated user by id + * @returns {User} + */ + public retrieveAuthenticatedUserById(userId: string): Observable { + return this.epersonService.findById(userId).pipe( + getAllSucceededRemoteDataPayload() + ) + } + + /** + * Returns the authenticated user from the store + * @returns {User} + */ + public getAuthenticatedUserFromStore(): Observable { + return this.store.pipe( + select(getAuthenticatedUserId), + hasValueOperator(), + switchMap((id: string) => this.epersonService.findById(id)), + getAllSucceededRemoteDataPayload() + ) + } + /** * Checks if token is present into browser storage and is valid. */ @@ -430,9 +460,9 @@ export class AuthService { * Refresh route navigated */ public refreshAfterLogout() { - this.router.navigate(['/home']); - // Hard redirect to home page, so that all state is definitely lost - this._window.nativeWindow.location.href = '/home'; + // Hard redirect to the reload page with a unique number behind it + // so that all state is definitely lost + this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`; } /** @@ -469,4 +499,51 @@ export class AuthService { this.storage.remove(REDIRECT_COOKIE); } + /** + * Start impersonating EPerson + * @param epersonId ID of the EPerson to impersonate + */ + impersonate(epersonId: string) { + this.storage.set(IMPERSONATING_COOKIE, epersonId); + this.refreshAfterLogout(); + } + + /** + * Stop impersonating EPerson + */ + stopImpersonating() { + this.storage.remove(IMPERSONATING_COOKIE); + } + + /** + * Stop impersonating EPerson and refresh the store/ui + */ + stopImpersonatingAndRefresh() { + this.stopImpersonating(); + this.refreshAfterLogout(); + } + + /** + * Get the ID of the EPerson we're currently impersonating + * Returns undefined if we're not impersonating anyone + */ + getImpersonateID(): string { + return this.storage.get(IMPERSONATING_COOKIE); + } + + /** + * Whether or not we are currently impersonating an EPerson + */ + isImpersonating(): boolean { + return hasValue(this.getImpersonateID()); + } + + /** + * Whether or not we are currently impersonating a specific EPerson + * @param epersonId ID of the EPerson to check + */ + isImpersonatingUser(epersonId: string): boolean { + return this.getImpersonateID() === epersonId; + } + } diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index 4e51bc1fc9..173f82e810 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -8,7 +8,6 @@ import { createSelector } from '@ngrx/store'; */ import { AuthState } from './auth.reducer'; import { AppState } from '../../app.reducer'; -import { EPerson } from '../eperson/models/eperson.model'; /** * Returns the user state. @@ -36,12 +35,11 @@ const _isAuthenticatedLoaded = (state: AuthState) => state.loaded; /** * Return the users state - * NOTE: when state is REHYDRATED user object lose prototype so return always a new EPerson object - * @function _getAuthenticatedUser + * @function _getAuthenticatedUserId * @param {State} state - * @returns {EPerson} + * @returns {string} User ID */ -const _getAuthenticatedUser = (state: AuthState) => Object.assign(new EPerson(), state.user); +const _getAuthenticatedUserId = (state: AuthState) => state.userId; /** * Returns the authentication error. @@ -119,13 +117,13 @@ const _getAuthenticationMethods = (state: AuthState) => state.authMethods; export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods); /** - * Returns the authenticated user - * @function getAuthenticatedUser + * Returns the authenticated user id + * @function getAuthenticatedUserId * @param {AuthState} state * @param {any} props - * @return {User} + * @return {string} User ID */ -export const getAuthenticatedUser = createSelector(getAuthState, _getAuthenticatedUser); +export const getAuthenticatedUserId = createSelector(getAuthState, _getAuthenticatedUserId); /** * Returns the authentication error. diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 7384a031db..80d7563637 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for a Collection */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { super(breadcrumbService, dataService); diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index d1f21455f2..298d69133f 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for a Community */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { super(breadcrumbService, dataService); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index 2a0005f548..92a50058db 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -1,6 +1,6 @@ import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { Collection } from '../shared/collection.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getTestScheduler } from 'jasmine-marbles'; import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 80e68a16f5..09292fec21 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -13,7 +13,9 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for a DSpaceObject */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export abstract class DSOBreadcrumbResolver implements Resolve> { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService) { } diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts index 101545cb14..5c31e40362 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts @@ -1,9 +1,9 @@ import { async, TestBed } from '@angular/core/testing'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { getMockLinkService } from '../../shared/mocks/mock-link-service'; +import { getMockLinkService } from '../../shared/mocks/link-service.mock'; import { LinkService } from '../cache/builders/link.service'; import { Item } from '../shared/item.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { DSpaceObject } from '../shared/dspace-object.model'; import { of as observableOf } from 'rxjs'; import { Community } from '../shared/community.model'; diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 3cb73be876..003c11bf83 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -15,7 +15,9 @@ import { Injectable } from '@angular/core'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class DSOBreadcrumbsService implements BreadcrumbsService { constructor( private linkService: LinkService, diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index 161c4f7254..5567137334 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -28,7 +28,8 @@ export class DSONameService { return dso.firstMetadataValue('organization.legalName'); }, Default: (dso: DSpaceObject): string => { - return dso.firstMetadataValue('dc.title'); + // If object doesn't have dc.title metadata use name property + return dso.firstMetadataValue('dc.title') || dso.name; } }; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index d34d6d8a9b..a06abdc816 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -14,7 +14,7 @@ describe('I18nBreadcrumbResolver', () => { }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any); + const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any); const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index de7d061a3f..a6298628c7 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -7,7 +7,9 @@ import { hasNoValue } from '../../shared/empty.util'; /** * The class that resolves a BreadcrumbConfig object with an i18n key string for a route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class I18nBreadcrumbResolver implements Resolve> { constructor(protected breadcrumbService: I18nBreadcrumbsService) { } @@ -23,7 +25,17 @@ export class I18nBreadcrumbResolver implements Resolve> if (hasNoValue(key)) { throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data') } - const fullPath = route.url.join(''); + const fullPath = this.getResolvedUrl(route); return { provider: this.breadcrumbService, key: key, url: fullPath }; } + + /** + * Resolve the full URL of an ActivatedRouteSnapshot + * @param route + */ + getResolvedUrl(route: ActivatedRouteSnapshot): string { + return route.pathFromRoot + .map((v) => v.url.map((segment) => segment.toString()).join('/')) + .join('/'); + } } diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index e07d9ed541..b774b58126 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -11,7 +11,9 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; /** * Service to calculate i18n breadcrumbs for a single part of the route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class I18nBreadcrumbsService implements BreadcrumbsService { /** diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index cd0c23cf82..8e13eda01d 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for an Item */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { super(breadcrumbService, dataService); diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 6dafa4cf0a..58796636e2 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -1,9 +1,9 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 4ba04bfa55..06a955cb00 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,6 +1,4 @@ -import 'reflect-metadata'; import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { DataService } from '../../data/data.service'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; @@ -98,7 +96,7 @@ export const link = ( let targetMap = linkMap.get(target.constructor); if (hasNoValue(targetMap)) { - targetMap = new Map>(); + targetMap = new Map>(); } if (hasNoValue(linkName)) { diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index e9b8447c22..95a7570207 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -40,10 +40,10 @@ class TestModel implements HALResource { @Injectable() class TestDataService { findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>) { - return 'findAllByHref' + return 'findAllByHref'; } findByHref(href: string, ...linksToFollow: Array>) { - return 'findByHref' + return 'findByHref'; } } @@ -169,7 +169,7 @@ describe('LinkService', () => { describe('resolveLinks', () => { beforeEach(() => { spyOn(service, 'resolveLink'); - service.resolveLinks(testModel, followLink('predecessor'), followLink('successor')) + result = service.resolveLinks(testModel, followLink('predecessor'), followLink('successor')) }); it('should call resolveLink with the model for each of the provided links', () => { diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index 85267d7f4c..8bf689d794 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -1,5 +1,5 @@ import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { Item } from '../../shared/item.model'; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index df895e11a2..6c9f40888f 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -8,7 +8,7 @@ import { isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -150,12 +150,7 @@ export class RemoteDataBuildService { filterSuccessfulResponses(), map((response: DSOSuccessResponse) => { if (hasValue((response as DSOSuccessResponse).pageInfo)) { - const resPageInfo = (response as DSOSuccessResponse).pageInfo; - if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { - return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); - } else { - return resPageInfo; - } + return (response as DSOSuccessResponse).pageInfo; } }) ); diff --git a/src/app/core/cache/models/search-param.model.ts b/src/app/core/cache/models/request-param.model.ts similarity index 86% rename from src/app/core/cache/models/search-param.model.ts rename to src/app/core/cache/models/request-param.model.ts index 3881dbe8b7..ac21fe0b8a 100644 --- a/src/app/core/cache/models/search-param.model.ts +++ b/src/app/core/cache/models/request-param.model.ts @@ -2,7 +2,7 @@ /** * Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object */ -export class SearchParam { +export class RequestParam { constructor(public fieldName: string, public fieldValue: any) { } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 3f46ecf647..b40965dd0a 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -6,9 +6,6 @@ import { ConfigObject } from '../config/models/config.model'; import { FacetValue } from '../../shared/search/facet-value.model'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; import { IntegrationModel } from '../integration/models/integration.model'; -import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; -import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; -import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -40,48 +37,6 @@ export class DSOSuccessResponse extends RestResponse { } } -/** - * A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse - */ -export class RegistryMetadataschemasSuccessResponse extends RestResponse { - constructor( - public metadataschemasResponse: RegistryMetadataschemasResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse - */ -export class RegistryMetadatafieldsSuccessResponse extends RestResponse { - constructor( - public metadatafieldsResponse: RegistryMetadatafieldsResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse - */ -export class RegistryBitstreamformatsSuccessResponse extends RestResponse { - constructor( - public bitstreamformatsResponse: RegistryBitstreamformatsResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - /** * A successful response containing exactly one MetadataSchema */ diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index c2aa7b14f9..245b6f67d8 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -5,10 +5,9 @@ import { cold, hot } from 'jasmine-marbles'; import { Observable, of as observableOf } from 'rxjs'; import * as operators from 'rxjs/operators'; -import { GLOBAL_CONFIG } from '../../../config'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { MockStore } from '../../shared/testing/mock-store'; -import { spyOnOperator } from '../../shared/testing/utils'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { StoreMock } from '../../shared/testing/store.mock'; +import { spyOnOperator } from '../../shared/testing/utils.test'; import { RequestService } from '../data/request.service'; import { RestRequestMethod } from '../data/rest-request-method'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -17,6 +16,7 @@ import { ObjectCacheService } from './object-cache.service'; import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; +import { storeModuleConfig } from '../../app.reducer'; describe('ServerSyncBufferEffects', () => { let ssbEffects: ServerSyncBufferEffects; @@ -37,12 +37,11 @@ describe('ServerSyncBufferEffects', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), ], providers: [ ServerSyncBufferEffects, provideMockActions(() => actions), - { provide: GLOBAL_CONFIG, useValue: testConfig }, { provide: RequestService, useValue: getMockRequestService() }, { provide: ObjectCacheService, useValue: { @@ -62,7 +61,7 @@ describe('ServerSyncBufferEffects', () => { } } }, - { provide: Store, useClass: MockStore } + { provide: Store, useClass: StoreMock } // other providers ], }); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 84f0312385..7d57bb4433 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,15 +1,8 @@ import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { coreSelector } from '../core.selectors'; -import { - AddToSSBAction, - CommitSSBAction, - EmptySSBAction, - ServerSyncBufferActionTypes -} from './server-sync-buffer.actions'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { AddToSSBAction, CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; import { CoreState } from '../core.reducers'; import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; @@ -21,6 +14,7 @@ import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { RestRequestMethod } from '../data/rest-request-method'; +import { environment } from '../../../environments/environment'; import { ObjectCacheEntry } from './object-cache.reducer'; import { Operation } from 'fast-json-patch'; @@ -37,7 +31,7 @@ export class ServerSyncBufferEffects { .pipe( ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { - const autoSyncConfig = this.EnvConfig.cache.autoSync; + const autoSyncConfig = environment.cache.autoSync; const timeoutInSeconds = autoSyncConfig.timePerMethod[action.payload.method] || autoSyncConfig.defaultTime; return observableOf(new CommitSSBAction(action.payload.method)).pipe( delay(timeoutInSeconds * 1000), @@ -85,7 +79,7 @@ export class ServerSyncBufferEffects { return observableOf({ type: 'NO_ACTION' }); } }) - ) + ); }) ); @@ -114,8 +108,7 @@ export class ServerSyncBufferEffects { constructor(private actions$: Actions, private store: Store, private requestService: RequestService, - private objectCache: ObjectCacheService, - @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { + private objectCache: ObjectCacheService) { } } diff --git a/src/app/core/config/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts index 87a7057078..c0bc8b3212 100644 --- a/src/app/core/config/config-response-parsing.service.spec.ts +++ b/src/app/core/config/config-response-parsing.service.spec.ts @@ -1,5 +1,4 @@ import { Store } from '@ngrx/store'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; @@ -13,12 +12,11 @@ import { SubmissionSectionModel } from './models/config-submission-section.model describe('ConfigResponseParsingService', () => { let service: ConfigResponseParsingService; - const EnvConfig = {} as GlobalConfig; const store = {} as Store; const objectCacheService = new ObjectCacheService(store, undefined); let validResponse; beforeEach(() => { - service = new ConfigResponseParsingService(EnvConfig, objectCacheService); + service = new ConfigResponseParsingService(objectCacheService); validResponse = { payload: { id: 'traditional', diff --git a/src/app/core/config/config-response-parsing.service.ts b/src/app/core/config/config-response-parsing.service.ts index d674445d54..a603de291a 100644 --- a/src/app/core/config/config-response-parsing.service.ts +++ b/src/app/core/config/config-response-parsing.service.ts @@ -8,8 +8,6 @@ import { isNotEmpty } from '../../shared/empty.util'; import { ConfigObject } from './models/config.model'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; @Injectable() @@ -18,7 +16,6 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp protected shouldDirectlyAttachEmbeds = true; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 402ee88b81..80febb711f 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,11 +1,11 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; import { ConfigRequest, FindListOptions } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 56062a0c41..28bf3a21b4 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -4,10 +4,10 @@ import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { EffectsModule } from '@ngrx/effects'; -import { StoreModule } from '@ngrx/store'; +import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; -import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config'; + import { isNotEmpty } from '../shared/empty.util'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; import { FormService } from '../shared/form/form.service'; @@ -16,9 +16,9 @@ import { MenuService } from '../shared/menu/menu.service'; import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; import { MOCK_RESPONSE_MAP, - MockResponseMap, + ResponseMapMock, mockResponseMap -} from '../shared/mocks/dspace-rest-v2/mocks/mock-response-map'; +} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service'; @@ -44,7 +44,7 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { coreEffects } from './core.effects'; -import { coreReducers } from './core.reducers'; +import { coreReducers, CoreState } from './core.reducers'; import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; @@ -69,16 +69,11 @@ import { ItemDataService } from './data/item-data.service'; import { LicenseDataService } from './data/license-data.service'; import { LookupRelationService } from './data/lookup-relation.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; -import { MetadatafieldParsingService } from './data/metadatafield-parsing.service'; -import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; -import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; -import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipService } from './data/relationship.service'; -import { ResourcePolicyService } from './data/resource-policy.service'; +import { ResourcePolicyService } from './resource-policy/resource-policy.service'; import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { SiteDataService } from './data/site-data.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; @@ -116,7 +111,7 @@ import { RelationshipType } from './shared/item-relationships/relationship-type. import { Relationship } from './shared/item-relationships/relationship.model'; import { Item } from './shared/item.model'; import { License } from './shared/license.model'; -import { ResourcePolicy } from './shared/resource-policy.model'; +import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; import { SearchConfigurationService } from './shared/search/search-configuration.service'; import { SearchFilterService } from './shared/search/search-filter.service'; import { SearchService } from './shared/search/search.service'; @@ -137,6 +132,8 @@ import { PoolTaskDataService } from './tasks/pool-task-data.service'; import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; import { BitstreamDataService } from './data/bitstream-data.service'; +import { environment } from '../../environments/environment'; +import { storeModuleConfig } from '../app.reducer'; import { VersionDataService } from './data/version-data.service'; import { VersionHistoryDataService } from './data/version-history-data.service'; import { Version } from './shared/version.model'; @@ -145,22 +142,24 @@ import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { ItemTemplateDataService } from './data/item-template-data.service'; import { TemplateItem } from './shared/template-item.model'; +import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; +import { MetadataFieldDataService } from './data/metadata-field-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes * If there is no mock version available for the endpoint, the actual REST response will be used just like in production mode */ -export const restServiceFactory = (cfg: GlobalConfig, mocks: MockResponseMap, http: HttpClient) => { - if (ENV_CONFIG.production) { +export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => { + if (environment.production) { return new DSpaceRESTv2Service(http); } else { - return new EndpointMockingRestService(cfg, mocks, http); + return new EndpointMockingRestService(mocks, http); } }; const IMPORTS = [ CommonModule, - StoreModule.forFeature('core', coreReducers, {}), + StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(coreEffects) ]; @@ -178,11 +177,7 @@ const PROVIDERS = [ SiteDataService, DSOResponseParsingService, { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, - { - provide: DSpaceRESTv2Service, - useFactory: restServiceFactory, - deps: [GLOBAL_CONFIG, MOCK_RESPONSE_MAP, HttpClient] - }, + { provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient]}, DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService, @@ -205,9 +200,6 @@ const PROVIDERS = [ FacetValueResponseParsingService, FacetValueMapResponseParsingService, FacetConfigResponseParsingService, - RegistryMetadataschemasResponseParsingService, - RegistryMetadatafieldsResponseParsingService, - RegistryBitstreamformatsResponseParsingService, MappedCollectionsReponseParsingService, DebugResponseParsingService, SearchResponseParsingService, @@ -227,8 +219,6 @@ const PROVIDERS = [ JsonPatchOperationsBuilder, AuthorityService, IntegrationResponseParsingService, - MetadataschemaParsingService, - MetadatafieldParsingService, UploaderService, UUIDService, NotificationsService, @@ -269,6 +259,8 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + MetadataSchemaDataService, + MetadataFieldDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/base-response-parsing.service.spec.ts b/src/app/core/data/base-response-parsing.service.spec.ts index a1d602dc65..85f531e0ea 100644 --- a/src/app/core/data/base-response-parsing.service.spec.ts +++ b/src/app/core/data/base-response-parsing.service.spec.ts @@ -1,5 +1,4 @@ import { BaseResponseParsingService } from './base-response-parsing.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CacheableObject } from '../cache/object-cache.reducer'; import { GetRequest, RestRequest } from './request.models'; @@ -9,8 +8,7 @@ import { DSpaceObject } from '../shared/dspace-object.model'; class TestService extends BaseResponseParsingService { toCache = true; - constructor(protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService) { + constructor(protected objectCache: ObjectCacheService) { super(); } @@ -26,24 +24,22 @@ class TestService extends BaseResponseParsingService { describe('BaseResponseParsingService', () => { let service: TestService; - let config: GlobalConfig; let objectCache: ObjectCacheService; const requestUUID = 'request-uuid'; const requestHref = 'request-href'; const request = new GetRequest(requestUUID, requestHref); + let obj: CacheableObject; beforeEach(() => { - config = Object.assign({}); + obj = undefined; objectCache = jasmine.createSpyObj('objectCache', { add: {} }); - service = new TestService(config, objectCache); + service = new TestService(objectCache); }); describe('cache', () => { - let obj: CacheableObject; - describe('when the object is undefined', () => { it('should not throw an error', () => { expect(() => { service.cache(obj, request, {}) }).not.toThrow(); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index efbe838d82..d69ebfbed5 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -4,11 +4,11 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { Serializer } from '../serializer'; import { PageInfo } from '../shared/page-info.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { getClassForType } from '../cache/builders/build-decorators'; import { RestRequest } from './request.models'; +import { environment } from '../../../environments/environment'; /* tslint:disable:max-classes-per-file */ /** @@ -35,7 +35,6 @@ export function isRestPaginatedList(halObj: any): boolean { } export abstract class BaseResponseParsingService { - protected abstract EnvConfig: GlobalConfig; protected abstract objectCache: ObjectCacheService; protected abstract toCache: boolean; protected shouldDirectlyAttachEmbeds = false; @@ -147,7 +146,7 @@ export abstract class BaseResponseParsingService { console.warn(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`); return; } - this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : this.EnvConfig.cache.msToLive.default, request.uuid); + this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid); } processPageInfo(payload: any): PageInfo { @@ -157,7 +156,7 @@ export abstract class BaseResponseParsingService { if (pageInfoObject.currentPage >= 0) { Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); } - return pageInfoObject + return pageInfoObject; } else { return undefined; } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index fca0f6b650..b328141d7b 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -2,14 +2,14 @@ import { BitstreamDataService } from './bitstream-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RequestService } from './request.service'; import { Bitstream } from '../shared/bitstream.model'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { BitstreamFormatDataService } from './bitstream-format-data.service'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level'; import { PutRequest } from './request.models'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; describe('BitstreamDataService', () => { let service: BitstreamDataService; diff --git a/src/app/core/data/browse-entries-response-parsing.service.spec.ts b/src/app/core/data/browse-entries-response-parsing.service.spec.ts index ef9a833765..3c7319d5cf 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.spec.ts @@ -1,4 +1,4 @@ -import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; @@ -8,7 +8,7 @@ describe('BrowseEntriesResponseParsingService', () => { let service: BrowseEntriesResponseParsingService; beforeEach(() => { - service = new BrowseEntriesResponseParsingService(undefined, getMockObjectCacheService()); + service = new BrowseEntriesResponseParsingService(getMockObjectCacheService()); }); describe('parse', () => { diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts index ec35b8cc75..98385f0237 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.ts @@ -1,6 +1,4 @@ import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; @@ -17,7 +15,6 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ protected toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/browse-items-response-parsing-service.spec.ts b/src/app/core/data/browse-items-response-parsing-service.spec.ts index 50b3be5de7..a1b1a14bff 100644 --- a/src/app/core/data/browse-items-response-parsing-service.spec.ts +++ b/src/app/core/data/browse-items-response-parsing-service.spec.ts @@ -1,4 +1,4 @@ -import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; @@ -9,7 +9,7 @@ describe('BrowseItemsResponseParsingService', () => { let service: BrowseItemsResponseParsingService; beforeEach(() => { - service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService()); + service = new BrowseItemsResponseParsingService(getMockObjectCacheService()); }); describe('parse', () => { diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index 08ade5772d..2b7ec647c9 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -1,7 +1,5 @@ import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; @@ -20,7 +18,6 @@ export class BrowseItemsResponseParsingService extends BaseResponseParsingServic protected toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 160ea0ff0d..de0e8a4337 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -88,10 +88,12 @@ export class BundleDataService extends DataService { /** * Get the bitstreams endpoint for a bundle * @param bundleId + * @param searchOptions */ - getBitstreamsEndpoint(bundleId: string): Observable { + getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable { return this.getBrowseEndpoint().pipe( - switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)) + switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)), + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) ); } @@ -102,9 +104,8 @@ export class BundleDataService extends DataService { * @param linksToFollow The {@link FollowLinkConfig}s for the request */ getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array>): Observable>> { - const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe( - map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) - ); + const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions); + hrefObs.pipe( take(1) ).subscribe((href) => { diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 96141d6a8a..7087655a26 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -1,10 +1,10 @@ import { CollectionDataService } from './collection-data.service'; import { RequestService } from './request.service'; import { TranslateService } from '@ngx-translate/core'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; -import { getMockTranslateService } from '../../shared/mocks/mock-translate.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { fakeAsync, tick } from '@angular/core/testing'; import { ContentSourceRequest, GetRequest, RequestError, UpdateContentSourceRequest } from './request.models'; import { ContentSource } from '../shared/content-source.model'; @@ -13,13 +13,19 @@ import { RequestEntry } from './request.reducer'; import { ErrorResponse, RestResponse } from '../cache/response.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Collection } from '../shared/collection.model'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedList } from './paginated-list'; +import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; +import { hot, getTestScheduler, cold } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; const url = 'fake-url'; const collectionId = 'fake-collection-id'; describe('CollectionDataService', () => { let service: CollectionDataService; - + let scheduler: TestScheduler; let requestService: RequestService; let translate: TranslateService; let notificationsService: any; @@ -27,6 +33,44 @@ describe('CollectionDataService', () => { let objectCache: ObjectCacheService; let halService: any; + const mockCollection1: Collection = Object.assign(new Collection(), { + id: 'test-collection-1-1', + name: 'test-collection-1', + _links: { + self: { + href: 'https://rest.api/collections/test-collection-1-1' + } + } + }); + + const mockCollection2: Collection = Object.assign(new Collection(), { + id: 'test-collection-2-2', + name: 'test-collection-2', + _links: { + self: { + href: 'https://rest.api/collections/test-collection-2-2' + } + } + }); + + const mockCollection3: Collection = Object.assign(new Collection(), { + id: 'test-collection-3-3', + name: 'test-collection-3', + _links: { + self: { + href: 'https://rest.api/collections/test-collection-3-3' + } + } + }); + + const queryString = 'test-string'; + const communityId = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const pageInfo = new PageInfo(); + const array = [mockCollection1, mockCollection2, mockCollection3]; + const paginatedList = new PaginatedList(pageInfo, array); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + describe('when the requests are successful', () => { beforeEach(() => { createService(); @@ -74,6 +118,43 @@ describe('CollectionDataService', () => { }); }); + describe('when calling getAuthorizedCollection', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn(service, 'getAuthorizedCollection').and.callThrough(); + spyOn(service, 'getAuthorizedCollectionByCommunity').and.callThrough(); + }); + + it('should proxy the call to getAuthorizedCollection', () => { + scheduler.schedule(() => service.getAuthorizedCollection(queryString)); + scheduler.flush(); + + expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString); + }); + + it('should return a RemoteData> for the getAuthorizedCollection', () => { + const result = service.getAuthorizedCollection(queryString) + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + + it('should proxy the call to getAuthorizedCollectionByCommunity', () => { + scheduler.schedule(() => service.getAuthorizedCollectionByCommunity(communityId, queryString)); + scheduler.flush(); + + expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString); + }); + + it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { + const result = service.getAuthorizedCollectionByCommunity(communityId, queryString) + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); }); describe('when the requests are unsuccessful', () => { @@ -117,7 +198,9 @@ describe('CollectionDataService', () => { function createService(requestEntry$?) { requestService = getMockRequestService(requestEntry$); rdbService = jasmine.createSpyObj('rdbService', { - buildList: jasmine.createSpy('buildList') + buildList: hot('a|', { + a: paginatedListRD + }) }); objectCache = jasmine.createSpyObj('objectCache', { remove: jasmine.createSpy('remove') diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 6ae40f4ca9..41f70dd31c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -12,7 +12,7 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; @@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService { /** * Get all collections the user is authorized to submit to * + * @param query limit the returned collection to those with metadata values matching the query terms. * @param options The [[FindListOptions]] object * @return Observable>> * collection list */ - getAuthorizedCollection(options: FindListOptions = {}): Observable>> { - const searchHref = 'findAuthorized'; + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + const searchHref = 'findSubmitAuthorized'; + options = Object.assign({}, options, { + searchParams: [new RequestParam('query', query)] + }); - return this.searchBy(searchHref, options).pipe( + return this.searchBy(searchHref, options, ...linksToFollow).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } @@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService { * Get all collections the user is authorized to submit to, by community * * @param communityId The community id + * @param query limit the returned collection to those with metadata values matching the query terms. * @param options The [[FindListOptions]] object * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable>> { - const searchHref = 'findAuthorizedByCommunity'; + getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable>> { + const searchHref = 'findSubmitAuthorizedByCommunity'; options = Object.assign({}, options, { - searchParams: [new SearchParam('uuid', communityId)] + searchParams: [ + new RequestParam('uuid', communityId), + new RequestParam('query', query) + ] }); return this.searchBy(searchHref, options).pipe( @@ -108,7 +116,7 @@ export class CollectionDataService extends ComColDataService { * true if the user has at least one collection to submit to */ hasAuthorizedCollection(): Observable { - const searchHref = 'findAuthorized'; + const searchHref = 'findSubmitAuthorized'; const options = new FindListOptions(); options.elementsPerPage = 1; diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index fc487527b9..1ba19df18c 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -3,8 +3,7 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { Observable, of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { GlobalConfig } from '../../../config'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -27,7 +26,6 @@ class TestService extends ComColDataService { protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -55,7 +53,6 @@ describe('ComColDataService', () => { const rdbService = {} as RemoteDataBuildService; const store = {} as Store; - const EnvConfig = {} as GlobalConfig; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; @@ -67,7 +64,7 @@ describe('ComColDataService', () => { const getRequestEntry$ = (successful: boolean) => { return observableOf({ response: { isSuccessful: successful } as any - } as RequestEntry) + } as RequestEntry); }; const communitiesEndpoint = 'https://rest.api/core/communities'; @@ -106,7 +103,6 @@ describe('ComColDataService', () => { requestService, rdbService, store, - EnvConfig, cds, objectCache, halService, diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index f776dfea63..a99fc54269 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -3,22 +3,21 @@ import { Store } from '@ngrx/store'; import { compare, Operation } from 'fast-json-patch'; import { Observable, of as observableOf } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; -import { Collection } from '../shared/collection.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ChangeAnalyzer } from './change-analyzer'; import { DataService } from './data.service'; import { FindListOptions, PatchRequest } from './request.models'; import { RequestService } from './request.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; const endpoint = 'https://rest.api/core'; @@ -54,23 +53,32 @@ class DummyChangeAnalyzer implements ChangeAnalyzer { describe('DataService', () => { let service: TestService; let options: FindListOptions; - const requestService = getMockRequestService(); - const halService = new HALEndpointServiceStub('url') as any; - const rdbService = {} as RemoteDataBuildService; - const notificationsService = {} as NotificationsService; - const http = {} as HttpClient; - const comparator = new DummyChangeAnalyzer() as any; - const objectCache = { - addPatch: () => { - /* empty */ - }, - getObjectBySelfLink: () => { - /* empty */ - } - } as any; - const store = {} as Store; + let requestService; + let halService; + let rdbService; + let notificationsService; + let http; + let comparator; + let objectCache; + let store; function initTestService(): TestService { + requestService = getMockRequestService(); + halService = new HALEndpointServiceStub('url') as any; + rdbService = {} as RemoteDataBuildService; + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = new DummyChangeAnalyzer() as any; + objectCache = { + + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + store = {} as Store; return new TestService( requestService, rdbService, @@ -84,7 +92,9 @@ describe('DataService', () => { ); } - service = initTestService(); + beforeEach(() => { + service = initTestService(); + }) describe('getFindAllHref', () => { @@ -151,67 +161,33 @@ describe('DataService', () => { }); it('should include single linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); const expected = `${endpoint}?embed=bundles`; - (service as any).getFindAllHref({}, null, mockFollowLinkConfig).subscribe((value) => { + (service as any).getFindAllHref({}, null, followLink('bundles')).subscribe((value) => { expect(value).toBe(expected); }); }); it('should include multiple linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`; - (service as any).getFindAllHref({}, null, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3).subscribe((value) => { + (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')).subscribe((value) => { expect(value).toBe(expected); }); }); it('should not include linksToFollow with shouldEmbed = false', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${endpoint}?embed=templateItemOf`; - (service as any).getFindAllHref({}, null, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3).subscribe((value) => { + (service as any).getFindAllHref({}, null, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')).subscribe((value) => { expect(value).toBe(expected); }); }); it('should include nested linksToFollow 3lvl', () => { - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'relationships' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'itemtemplate' as any, - linksToFollow: mockFollowLinkConfig3, - }); - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - linksToFollow: mockFollowLinkConfig2, - }); const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`; - (service as any).getFindAllHref({}, null, mockFollowLinkConfig).subscribe((value) => { + (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))).subscribe((value) => { expect(value).toBe(expected); }); }); @@ -227,60 +203,26 @@ describe('DataService', () => { }); it('should include single linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); const expected = `${endpointMock}/${resourceIdMock}?embed=bundles`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig); + const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles')); expect(result).toEqual(expected); }); it('should include multiple linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${endpointMock}/${resourceIdMock}?embed=bundles&embed=owningCollection&embed=templateItemOf`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3); + const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')); expect(result).toEqual(expected); }); it('should not include linksToFollow with shouldEmbed = false', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3); + const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')); expect(result).toEqual(expected); }); it('should include nested linksToFollow 3lvl', () => { - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'relationships' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'itemtemplate' as any, - linksToFollow: mockFollowLinkConfig3, - }); - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - linksToFollow: mockFollowLinkConfig2, - }); const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig); + const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); expect(result).toEqual(expected); }); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ff228e08fc..b6f6465450 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -20,7 +20,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { getClassForType } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, RestResponse } from '../cache/response.models'; @@ -45,12 +45,13 @@ import { FindListOptions, FindListRequest, GetRequest, - PatchRequest + PatchRequest, PutRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; import { UpdateDataService } from './update-data.service'; +import { GenericConstructor } from '../shared/generic-constructor'; export abstract class DataService implements UpdateDataService { protected abstract requestService: RequestService; @@ -119,7 +120,7 @@ export abstract class DataService implements UpdateDa result$ = this.getSearchEndpoint(searchMethod); if (hasValue(options.searchParams)) { - options.searchParams.forEach((param: SearchParam) => { + options.searchParams.forEach((param: RequestParam) => { args.push(`${param.fieldName}=${param.fieldValue}`); }) } @@ -161,6 +162,33 @@ export abstract class DataService implements UpdateDa } } + /** + * Turn an array of RequestParam into a query string and combine it with the given HREF + * + * @param href The HREF to which the query string should be appended + * @param params Array with additional params to combine with query string + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * + * @return {Observable} + * Return an observable that emits created HREF + */ + protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: Array>): string { + + let args = []; + if (hasValue(params)) { + params.forEach((param: RequestParam) => { + args.push(`${param.fieldName}=${param.fieldValue}`); + }) + } + + args = this.addEmbedParams(args, ...linksToFollow); + + if (isNotEmpty(args)) { + return new URLCombiner(href, `?${args.join('&')}`).toString(); + } else { + return href; + } + } /** * Adds the embed options to the link for the request * @param args params for the query string @@ -310,9 +338,9 @@ export abstract class DataService implements UpdateDa * @param searchMethod The search method for the object */ protected getSearchEndpoint(searchMethod: string): Observable { - return this.halService.getEndpoint(`${this.linkPath}/search`).pipe( + return this.halService.getEndpoint(this.linkPath).pipe( filter((href: string) => isNotEmpty(href)), - map((href: string) => `${href}/${searchMethod}`)); + map((href: string) => `${href}/search/${searchMethod}`)); } /** @@ -333,7 +361,9 @@ export abstract class DataService implements UpdateDa tap((href: string) => { this.requestService.removeByHrefSubstring(href); const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - request.responseMsToLive = 10 * 1000; + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } this.requestService.configure(request); } @@ -371,6 +401,28 @@ export abstract class DataService implements UpdateDa ); } + /** + * Send a PUT request for the specified object + * + * @param object The object to send a put request for. + */ + put(object: T): Observable> { + const requestId = this.requestService.generateRequestId(); + const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); + const request = new PutRequest(requestId, object._links.self.href, serializedObject); + + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + + this.requestService.configure(request); + + return this.requestService.getByUUID(requestId).pipe( + find((re: RequestEntry) => hasValue(re) && re.completed), + switchMap(() => this.findByHref(object._links.self.href)) + ); + } + /** * Add a new patch to the object cache * The patch is derived from the differences between the given object and its version in the object cache @@ -397,15 +449,15 @@ export abstract class DataService implements UpdateDa * * @param {DSpaceObject} dso * The object to create - * @param {string} parentUUID - * The UUID of the parent to create the new object under + * @param {RequestParam[]} params + * Array with additional params to combine with query string */ - create(dso: T, parentUUID?: string): Observable> { + create(dso: T, ...params: RequestParam[]): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.getEndpoint().pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) + map((endpoint: string) => this.buildHrefWithParams(endpoint, params)) ); const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); @@ -496,7 +548,7 @@ export abstract class DataService implements UpdateDa const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), + find((request: RequestEntry) => isNotEmpty(request) && request.completed), map((request: RequestEntry) => request.response.isSuccessful) ); } diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts index 4ef5bcb8b4..ca62347883 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -3,13 +3,11 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; -import { Collection } from '../shared/collection.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; import { DsoRedirectDataService } from './dso-redirect-data.service'; import { FindByIDRequest, IdentifierType } from './request.models'; import { RequestService } from './request.service'; @@ -34,12 +32,12 @@ describe('DsoRedirectDataService', () => { const http = {} as HttpClient; const comparator = {} as any; const objectCache = {} as ObjectCacheService; - let setup; + beforeEach(() => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', {a: pidLink}) + getEndpoint: cold('a', { a: pidLink }) }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, @@ -60,29 +58,26 @@ describe('DsoRedirectDataService', () => { } }; - setup = () => { - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: cold('a', { - a: remoteData - }) - }); - service = new DsoRedirectDataService( - requestService, - rdbService, - store, - objectCache, - halService, - notificationsService, - http, - comparator, - router - ); - } + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: remoteData + }) + }); + service = new DsoRedirectDataService( + requestService, + rdbService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + router + ); }); describe('findById', () => { it('should call HALEndpointService with the path to the pid endpoint', () => { - setup(); scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); @@ -90,7 +85,6 @@ describe('DsoRedirectDataService', () => { }); it('should call HALEndpointService with the path to the dso endpoint', () => { - setup(); scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.flush(); @@ -98,7 +92,6 @@ describe('DsoRedirectDataService', () => { }); it('should call HALEndpointService with the path to the dso endpoint when identifier type not specified', () => { - setup(); scheduler.schedule(() => service.findByIdAndIDType(dsoUUID)); scheduler.flush(); @@ -106,7 +99,6 @@ describe('DsoRedirectDataService', () => { }); it('should configure the proper FindByIDRequest for uuid', () => { - setup(); scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.flush(); @@ -114,7 +106,6 @@ describe('DsoRedirectDataService', () => { }); it('should configure the proper FindByIDRequest for handle', () => { - setup(); scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); @@ -123,7 +114,6 @@ describe('DsoRedirectDataService', () => { it('should navigate to item route', () => { remoteData.payload.type = 'item'; - setup(); const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); // The framework would normally subscribe but do it here so we can test navigation. redir.subscribe(); @@ -134,7 +124,6 @@ describe('DsoRedirectDataService', () => { it('should navigate to collections route', () => { remoteData.payload.type = 'collection'; - setup(); const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); redir.subscribe(); scheduler.schedule(() => redir); @@ -144,7 +133,6 @@ describe('DsoRedirectDataService', () => { it('should navigate to communities route', () => { remoteData.payload.type = 'community'; - setup(); const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); redir.subscribe(); scheduler.schedule(() => redir); @@ -160,60 +148,26 @@ describe('DsoRedirectDataService', () => { }); it('should include single linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); const expected = `${requestUUIDURL}&embed=bundles`; - const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig); + const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles')); expect(result).toEqual(expected); }); it('should include multiple linksToFollow as embed', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${requestUUIDURL}&embed=bundles&embed=owningCollection&embed=templateItemOf`; - const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3); + const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')); expect(result).toEqual(expected); }); it('should not include linksToFollow with shouldEmbed = false', () => { - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'bundles' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - shouldEmbed: false, - }); - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'templateItemOf' as any, - }); const expected = `${requestUUIDURL}&embed=templateItemOf`; - const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3); + const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')); expect(result).toEqual(expected); }); it('should include nested linksToFollow 3lvl', () => { - const mockFollowLinkConfig3: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'relationships' as any, - }); - const mockFollowLinkConfig2: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'itemtemplate' as any, - linksToFollow: mockFollowLinkConfig3, - }); - const mockFollowLinkConfig: FollowLinkConfig = Object.assign(new FollowLinkConfig(), { - name: 'owningCollection' as any, - linksToFollow: mockFollowLinkConfig2, - }); const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`; - const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig); + const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, followLink('itemtemplate', undefined, true, followLink('relationships')))); expect(result).toEqual(expected); }); }); diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 83676ce105..a4d4941bc8 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -1,8 +1,6 @@ import { Inject, Injectable } from '@angular/core'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; import { RestRequest } from './request.models'; @@ -17,7 +15,6 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem protected toCache = true; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts index 080c665ccf..5516e83c07 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,6 +1,4 @@ import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ResponseParsingService } from './parsing.service'; @@ -9,10 +7,6 @@ import { isNotEmpty } from '../../shared/empty.util'; @Injectable() export class EndpointMapResponseParsingService implements ResponseParsingService { - constructor( - @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, - ) { - } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts index f891b46883..7c7d676bc7 100644 --- a/src/app/core/data/external-source.service.spec.ts +++ b/src/app/core/data/external-source.service.spec.ts @@ -1,5 +1,6 @@ import { ExternalSourceService } from './external-source.service'; -import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { of as observableOf } from 'rxjs'; import { GetRequest } from './request.models'; diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index 3fc14b6495..0a552365f6 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,6 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { Injectable } from '@angular/core'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { FacetConfigSuccessResponse, RestResponse } from '../cache/response.models'; @@ -14,7 +12,6 @@ import { RestRequest } from './request.models'; export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index 8c8c12dff7..7845e44e57 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -1,14 +1,7 @@ -import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { Injectable } from '@angular/core'; import { FacetValue } from '../../shared/search/facet-value.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { - FacetValueMap, - FacetValueMapSuccessResponse, - FacetValueSuccessResponse, - RestResponse -} from '../cache/response.models'; +import { FacetValueMap, FacetValueMapSuccessResponse, FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { BaseResponseParsingService } from './base-response-parsing.service'; @@ -20,7 +13,6 @@ export class FacetValueMapResponseParsingService extends BaseResponseParsingServ toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index c9ff93a1ae..b56bedd1bb 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,6 +1,4 @@ import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { FacetValue } from '../../shared/search/facet-value.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; @@ -14,7 +12,6 @@ import { RestRequest } from './request.models'; export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts index d81ce4b6bd..1934afba27 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts @@ -1,5 +1,5 @@ import { FilteredDiscoveryPageResponseParsingService } from './filtered-discovery-page-response-parsing.service'; -import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from './parsing.service'; import { GetRequest } from './request.models'; @@ -10,7 +10,7 @@ describe('FilteredDiscoveryPageResponseParsingService', () => { let service: FilteredDiscoveryPageResponseParsingService; beforeEach(() => { - service = new FilteredDiscoveryPageResponseParsingService(undefined, getMockObjectCacheService()); + service = new FilteredDiscoveryPageResponseParsingService(getMockObjectCacheService()); }); describe('parse', () => { diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts index 166a915b16..02ce102ca6 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -4,8 +4,6 @@ import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; import { FilteredDiscoveryQueryResponse, RestResponse } from '../cache/response.models'; /** @@ -17,7 +15,6 @@ export class FilteredDiscoveryPageResponseParsingService extends BaseResponsePar objectFactory = {}; toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); } diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 2519c90973..282a43ec61 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -3,7 +3,7 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { Observable, of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BrowseService } from '../browse/browse.service'; import { ObjectCacheService } from '../cache/object-cache.service'; diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index c9fc7fc50d..ffeb6f9128 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -1,7 +1,8 @@ import { LookupRelationService } from './lookup-relation.service'; import { ExternalSourceService } from './external-source.service'; import { SearchService } from '../shared/search/search.service'; -import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { PaginatedList } from './paginated-list'; import { PageInfo } from '../shared/page-info.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; diff --git a/src/app/core/data/metadata-field-data.service.spec.ts b/src/app/core/data/metadata-field-data.service.spec.ts new file mode 100644 index 0000000000..5c17b56845 --- /dev/null +++ b/src/app/core/data/metadata-field-data.service.spec.ts @@ -0,0 +1,114 @@ +import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RestResponse } from '../cache/response.models'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { CreateRequest, FindListOptions, PutRequest } from './request.models'; +import { MetadataFieldDataService } from './metadata-field-data.service'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RequestParam } from '../cache/models/request-param.model'; + +describe('MetadataFieldDataService', () => { + let metadataFieldService: MetadataFieldDataService; + let requestService: RequestService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let schema: MetadataSchema; + let rdbService: RemoteDataBuildService; + + const endpoint = 'api/metadatafield/endpoint'; + + function init() { + schema = Object.assign(new MetadataSchema(), { + prefix: 'dc', + namespace: 'namespace', + _links: { + self: { href: 'selflink' } + } + }); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', + configure: {}, + getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), + removeByHrefSubstring: {} + }); + halService = Object.assign(new HALEndpointServiceStub(endpoint)); + notificationsService = jasmine.createSpyObj('notificationsService', { + error: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$(undefined) + }); + metadataFieldService = new MetadataFieldDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService); + } + + beforeEach(() => { + init(); + }); + + describe('findBySchema', () => { + beforeEach(() => { + spyOn(metadataFieldService, 'searchBy'); + }); + + it('should call searchBy with the correct arguments', () => { + metadataFieldService.findBySchema(schema); + const expectedOptions = Object.assign(new FindListOptions(), { + searchParams: [new RequestParam('schema', schema.prefix)] + }); + expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions); + }); + }); + + describe('createOrUpdateMetadataField', () => { + let field: MetadataField; + + beforeEach(() => { + field = Object.assign(new MetadataField(), { + element: 'identifier', + qualifier: undefined, + schema: schema, + _links: { + self: { href: 'selflink' } + } + }); + }); + + describe('called with a new metadata field', () => { + it('should send a CreateRequest', (done) => { + metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest)); + done(); + }); + }); + }); + + describe('called with an existing metadata field', () => { + beforeEach(() => { + field = Object.assign(field, { + id: 'id-of-existing-field' + }); + }); + + it('should send a PutRequest', (done) => { + metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + done(); + }); + }); + }); + }); + + describe('clearRequests', () => { + it('should remove requests on the data service\'s endpoint', (done) => { + metadataFieldService.clearRequests().subscribe(() => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts new file mode 100644 index 0000000000..f50be20f13 --- /dev/null +++ b/src/app/core/data/metadata-field-data.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; +import { DataService } from './data.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { FindListOptions, FindListRequest } from './request.models'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { hasValue } from '../../shared/empty.util'; +import { find, skipWhile, switchMap, tap } from 'rxjs/operators'; +import { RemoteData } from './remote-data'; +import { RequestParam } from '../cache/models/request-param.model'; +import { PaginatedList } from './paginated-list'; + +/** + * A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint + */ +@Injectable() +@dataService(METADATA_FIELD) +export class MetadataFieldDataService extends DataService { + protected linkPath = 'metadatafields'; + protected searchBySchemaLinkPath = 'bySchema'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService, + protected comparator: DefaultChangeAnalyzer, + protected http: HttpClient, + protected notificationsService: NotificationsService) { + super(); + } + + /** + * Find metadata fields belonging to a metadata schema + * @param schema The metadata schema to list fields for + * @param options The options info used to retrieve the fields + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array>) { + const optionsWithSchema = Object.assign(new FindListOptions(), options, { + searchParams: [new RequestParam('schema', schema.prefix)] + }); + return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow); + } + + /** + * Create or Update a MetadataField + * If the MetadataField contains an id, it is assumed the field already exists and is updated instead + * Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint): + * - On creation, a CreateRequest is used + * - On update, a PutRequest is used + * @param field The MetadataField to create or update + */ + createOrUpdateMetadataField(field: MetadataField): Observable> { + const isUpdate = hasValue(field.id); + + if (isUpdate) { + return this.put(field); + } else { + return this.create(field, new RequestParam('schemaId', field.schema.id)); + } + } + + /** + * Clear all metadata field requests + * Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema + */ + clearRequests(): Observable { + return this.getBrowseEndpoint().pipe( + tap((href: string) => { + this.requestService.removeByHrefSubstring(href); + }) + ); + } + +} diff --git a/src/app/core/data/metadata-schema-data.service.spec.ts b/src/app/core/data/metadata-schema-data.service.spec.ts new file mode 100644 index 0000000000..bf73deecb7 --- /dev/null +++ b/src/app/core/data/metadata-schema-data.service.spec.ts @@ -0,0 +1,89 @@ +import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { MetadataSchemaDataService } from './metadata-schema-data.service'; +import { of as observableOf } from 'rxjs'; +import { RestResponse } from '../cache/response.models'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { CreateRequest, PutRequest } from './request.models'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; + +describe('MetadataSchemaDataService', () => { + let metadataSchemaService: MetadataSchemaDataService; + let requestService: RequestService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let rdbService: RemoteDataBuildService; + + const endpoint = 'api/metadataschema/endpoint'; + + function init() { + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', + configure: {}, + getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }), + removeByHrefSubstring: {} + }); + halService = Object.assign(new HALEndpointServiceStub(endpoint)); + notificationsService = jasmine.createSpyObj('notificationsService', { + error: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$(undefined) + }); + metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService); + } + + beforeEach(() => { + init(); + }); + + describe('createOrUpdateMetadataSchema', () => { + let schema: MetadataSchema; + + beforeEach(() => { + schema = Object.assign(new MetadataSchema(), { + prefix: 'dc', + namespace: 'namespace', + _links: { + self: { href: 'selflink' } + } + }); + }); + + describe('called with a new metadata schema', () => { + it('should send a CreateRequest', (done) => { + metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest)); + done(); + }); + }); + }); + + describe('called with an existing metadata schema', () => { + beforeEach(() => { + schema = Object.assign(schema, { + id: 'id-of-existing-schema' + }); + }); + + it('should send a PutRequest', (done) => { + metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + done(); + }); + }); + }); + }); + + describe('clearRequests', () => { + it('should remove requests on the data service\'s endpoint', (done) => { + metadataSchemaService.clearRequests().subscribe(() => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataSchemaService as any).linkPath}`); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 915f588379..99a3f98b8e 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -9,37 +9,21 @@ import { CoreState } from '../core.reducers'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ChangeAnalyzer } from './change-analyzer'; - import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RequestService } from './request.service'; - -/* tslint:disable:max-classes-per-file */ -class DataServiceImpl extends DataService { - protected linkPath = 'metadataschemas'; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: ChangeAnalyzer) { - super(); - } - -} +import { Observable } from 'rxjs/internal/Observable'; +import { hasValue } from '../../shared/empty.util'; +import { tap } from 'rxjs/operators'; +import { RemoteData } from './remote-data'; /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ @Injectable() @dataService(METADATA_SCHEMA) -export class MetadataSchemaDataService { - private dataService: DataServiceImpl; +export class MetadataSchemaDataService extends DataService { + protected linkPath = 'metadataschemas'; constructor( protected requestService: RequestService, @@ -50,6 +34,35 @@ export class MetadataSchemaDataService { protected comparator: DefaultChangeAnalyzer, protected http: HttpClient, protected notificationsService: NotificationsService) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + super(); } + + /** + * Create or Update a MetadataSchema + * If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead + * Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint): + * - On creation, a CreateRequest is used + * - On update, a PutRequest is used + * @param schema The MetadataSchema to create or update + */ + createOrUpdateMetadataSchema(schema: MetadataSchema): Observable> { + const isUpdate = hasValue(schema.id); + + if (isUpdate) { + return this.put(schema); + } else { + return this.create(schema); + } + } + + /** + * Clear all metadata schema requests + * Used for refreshing lists after adding/updating/removing a metadata schema in the registry + */ + clearRequests(): Observable { + return this.getBrowseEndpoint().pipe( + tap((href: string) => this.requestService.removeByHrefSubstring(href)) + ); + } + } diff --git a/src/app/core/data/metadatafield-parsing.service.ts b/src/app/core/data/metadatafield-parsing.service.ts deleted file mode 100644 index 08f7892ac7..0000000000 --- a/src/app/core/data/metadatafield-parsing.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { MetadataField } from '../metadata/metadata-field.model'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -/** - * A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse - */ -@Injectable() -export class MetadatafieldParsingService implements ResponseParsingService { - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload); - return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText); - } - -} diff --git a/src/app/core/data/metadataschema-parsing.service.ts b/src/app/core/data/metadataschema-parsing.service.ts deleted file mode 100644 index f4b90e5dcd..0000000000 --- a/src/app/core/data/metadataschema-parsing.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class MetadataschemaParsingService implements ResponseParsingService { - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload); - return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText); - } - -} diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index 94918157ee..f26be768b1 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m */ export const ObjectUpdatesActionTypes = { INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'), - ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'), SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'), SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'), ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'), @@ -17,8 +16,7 @@ export const ObjectUpdatesActionTypes = { REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'), REMOVE: type('dspace/core/cache/object-updates/REMOVE'), REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'), - REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'), - MOVE: type('dspace/core/cache/object-updates/MOVE'), + REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD') }; /* tslint:disable:max-classes-per-file */ @@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = { export enum FieldChangeType { UPDATE = 0, ADD = 1, - REMOVE = 2, - MOVE = 3 + REMOVE = 2 } /** @@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action { payload: { url: string, fields: Identifiable[], - lastModified: Date, - order: string[], - pageSize: number, - page: number + lastModified: Date }; /** @@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action { constructor( url: string, fields: Identifiable[], - lastModified: Date, - order: string[] = [], - pageSize: number = 9999, - page: number = 0 + lastModified: Date ) { - this.payload = { url, fields, lastModified, order, pageSize, page }; - } -} - -/** - * An ngrx action to initialize a new page's fields in the ObjectUpdates state - */ -export class AddPageToCustomOrderAction implements Action { - type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER; - payload: { - url: string, - fields: Identifiable[], - order: string[], - page: number - }; - - /** - * Create a new AddPageToCustomOrderAction - * - * @param url The unique url of the page for which the fields are being added - * @param fields The identifiable fields of which the updates are kept track of - * @param order A custom order to keep track of objects moving around - * @param page The page to populate in the custom order - */ - constructor( - url: string, - fields: Identifiable[], - order: string[] = [], - page: number = 0 - ) { - this.payload = { url, fields, order, page }; + this.payload = { url, fields, lastModified }; } } @@ -320,43 +281,6 @@ export class RemoveFieldUpdateAction implements Action { } } -/** - * An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid - */ -export class MoveFieldUpdateAction implements Action { - type = ObjectUpdatesActionTypes.MOVE; - payload: { - url: string, - from: number, - to: number, - fromPage: number, - toPage: number, - field?: Identifiable - }; - - /** - * Create a new RemoveObjectUpdatesAction - * - * @param url - * the unique url of the page for which a field's change should be removed - * @param from The index of the object to move - * @param to The index to move the object to - * @param fromPage The page to move the object from - * @param toPage The page to move the object to - * @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages) - */ - constructor( - url: string, - from: number, - to: number, - fromPage: number, - toPage: number, - field?: Identifiable - ) { - this.payload = { url, from, to, fromPage, toPage, field }; - } -} - /* tslint:enable:max-classes-per-file */ /** @@ -369,8 +293,6 @@ export type ObjectUpdatesAction | ReinstateObjectUpdatesAction | RemoveObjectUpdatesAction | RemoveFieldUpdateAction - | MoveFieldUpdateAction - | AddPageToCustomOrderAction | RemoveAllObjectUpdatesAction | SelectVirtualMetadataAction | SetEditableFieldUpdateAction diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index bdf202049e..cb7f44039c 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -1,9 +1,9 @@ import * as deepFreeze from 'deep-freeze'; import { - AddFieldUpdateAction, AddPageToCustomOrderAction, + AddFieldUpdateAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, MoveFieldUpdateAction, + InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction, RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction @@ -85,16 +85,6 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, - customOrder: { - initialOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - newOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - pageSize: 10, - changed: false - } } }; @@ -121,16 +111,6 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, - customOrder: { - initialOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - newOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - pageSize: 10, - changed: false - } }, [url + OBJECT_UPDATES_TRASH_PATH]: { fieldStates: { @@ -165,16 +145,6 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, - customOrder: { - initialOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - newOrderPages: [ - { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } - ], - pageSize: 10, - changed: false - } } }; @@ -243,7 +213,7 @@ describe('objectUpdatesReducer', () => { }); it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { - const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0); + const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate); const expectedState = { [url]: { @@ -261,17 +231,7 @@ describe('objectUpdatesReducer', () => { }, fieldUpdates: {}, virtualMetadataSources: {}, - lastModified: modDate, - customOrder: { - initialOrderPages: [ - { order: [identifiable1.uuid, identifiable3.uuid] } - ], - newOrderPages: [ - { order: [identifiable1.uuid, identifiable3.uuid] } - ], - pageSize: 10, - changed: false - } + lastModified: modDate } }; const newState = objectUpdatesReducer(testState, action); @@ -337,30 +297,4 @@ describe('objectUpdatesReducer', () => { const newState = objectUpdatesReducer(testState, action); expect(newState[url].fieldUpdates[uuid]).toBeUndefined(); }); - - it('should move the custom order from the state when the MOVE action is dispatched', () => { - const action = new MoveFieldUpdateAction(url, 0, 1, 0, 0); - - const newState = objectUpdatesReducer(testState, action); - expect(newState[url].customOrder.newOrderPages[0].order[0]).toEqual(testState[url].customOrder.newOrderPages[0].order[1]); - expect(newState[url].customOrder.newOrderPages[0].order[1]).toEqual(testState[url].customOrder.newOrderPages[0].order[0]); - expect(newState[url].customOrder.changed).toEqual(true); - }); - - it('should add a new page to the custom order and add empty pages in between when the ADD_PAGE_TO_CUSTOM_ORDER action is dispatched', () => { - const identifiable4 = { - uuid: 'a23eae5a-7857-4ef9-8e52-989436ad2955', - key: 'dc.description.abstract', - language: null, - value: 'Extra value' - }; - const action = new AddPageToCustomOrderAction(url, [identifiable4], [identifiable4.uuid], 2); - - const newState = objectUpdatesReducer(testState, action); - // Confirm the page in between the two pages (index 1) has been filled with 10 (page size) undefined values - expect(newState[url].customOrder.newOrderPages[1].order.length).toEqual(10); - expect(newState[url].customOrder.newOrderPages[1].order[0]).toBeUndefined(); - // Verify the new page is correct - expect(newState[url].customOrder.newOrderPages[2].order[0]).toEqual(identifiable4.uuid); - }); }); diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 759a9f5c87..b1626a5ff5 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -1,8 +1,8 @@ import { - AddFieldUpdateAction, AddPageToCustomOrderAction, + AddFieldUpdateAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, MoveFieldUpdateAction, + InitializeFieldsAction, ObjectUpdatesAction, ObjectUpdatesActionTypes, ReinstateObjectUpdatesAction, @@ -12,9 +12,7 @@ import { SetValidFieldUpdateAction, SelectVirtualMetadataAction, } from './object-updates.actions'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; -import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; -import { from } from 'rxjs/internal/observable/from'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; import {Relationship} from '../../shared/item-relationships/relationship.model'; /** @@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship { keepRightVirtualMetadata: boolean, } -/** - * A custom order given to the list of objects - */ -export interface CustomOrder { - initialOrderPages: OrderPage[], - newOrderPages: OrderPage[], - pageSize: number; - changed: boolean -} - -export interface OrderPage { - order: string[] -} - /** * The updated state of a single page */ @@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry { fieldUpdates: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; - customOrder: CustomOrder } /** @@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: { return initializeFieldsUpdate(state, action as InitializeFieldsAction); } - case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: { - return addPageToCustomOrder(state, action as AddPageToCustomOrderAction); - } case ObjectUpdatesActionTypes.ADD_FIELD: { return addFieldUpdate(state, action as AddFieldUpdateAction); } @@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.SET_VALID_FIELD: { return setValidFieldUpdate(state, action as SetValidFieldUpdateAction); } - case ObjectUpdatesActionTypes.MOVE: { - return moveFieldUpdate(state, action as MoveFieldUpdateAction); - } default: { return state; } @@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { const url: string = action.payload.url; const fields: Identifiable[] = action.payload.fields; const lastModifiedServer: Date = action.payload.lastModified; - const order = action.payload.order; - const pageSize = action.payload.pageSize; - const page = action.payload.page; const fieldStates = createInitialFieldStates(fields); - const initialOrderPages = addOrderToPages([], order, pageSize, page); const newPageState = Object.assign( {}, state[url], { fieldStates: fieldStates }, { fieldUpdates: {} }, { virtualMetadataSources: {} }, - { lastModified: lastModifiedServer }, - { customOrder: { - initialOrderPages: initialOrderPages, - newOrderPages: initialOrderPages, - pageSize: pageSize, - changed: false } - } + { lastModified: lastModifiedServer } ); return Object.assign({}, state, { [url]: newPageState }); } -/** - * Add a page of objects to the state of a specific url and update a specific page of the custom order - * @param state The current state - * @param action The action to perform on the current state - */ -function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) { - const url: string = action.payload.url; - const fields: Identifiable[] = action.payload.fields; - const fieldStates = createInitialFieldStates(fields); - const order = action.payload.order; - const page = action.payload.page; - const pageState: ObjectUpdatesEntry = state[url] || {}; - const newPageState = Object.assign({}, pageState, { - fieldStates: Object.assign({}, pageState.fieldStates, fieldStates), - customOrder: Object.assign({}, pageState.customOrder, { - newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page), - initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page) - }) - }); - return Object.assign({}, state, { [url]: newPageState }); -} - /** * Add a new update for a specific field to the store * @param state The current state @@ -338,19 +283,9 @@ function discardObjectUpdatesFor(url: string, state: any) { } }); - const newCustomOrder = Object.assign({}, pageState.customOrder); - if (pageState.customOrder.changed) { - const initialOrder = pageState.customOrder.initialOrderPages; - if (isNotEmpty(initialOrder)) { - newCustomOrder.newOrderPages = initialOrder; - newCustomOrder.changed = false; - } - } - const discardedPageState = Object.assign({}, pageState, { fieldUpdates: {}, - fieldStates: newFieldStates, - customOrder: newCustomOrder + fieldStates: newFieldStates }); return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); } @@ -504,121 +439,3 @@ function createInitialFieldStates(fields: Identifiable[]) { uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState); return fieldStates; } - -/** - * Method to add a list of objects to an existing FieldStates object - * @param fieldStates FieldStates to add states to - * @param fields Identifiable objects The list of objects to add to the FieldStates - */ -function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) { - const uuids = fields.map((field: Identifiable) => field.uuid); - uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState); - return fieldStates; -} - -/** - * Move an object within the custom order of a page state - * @param state The current state - * @param action The move action to perform - */ -function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) { - const url = action.payload.url; - const fromIndex = action.payload.from; - const toIndex = action.payload.to; - const fromPage = action.payload.fromPage; - const toPage = action.payload.toPage; - const field = action.payload.field; - - const pageState: ObjectUpdatesEntry = state[url]; - const initialOrderPages = pageState.customOrder.initialOrderPages; - const customOrderPages = [...pageState.customOrder.newOrderPages]; - - // Create a copy of the custom orders for the from- and to-pages - const fromPageOrder = [...customOrderPages[fromPage].order]; - const toPageOrder = [...customOrderPages[toPage].order]; - if (fromPage === toPage) { - if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) { - // Move an item from one index to another within the same page - moveItemInArray(fromPageOrder, fromIndex, toIndex); - // Update the custom order for this page - customOrderPages[fromPage] = { order: fromPageOrder }; - } - } else { - if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) { - // Move an item from one index of one page to an index in another page - transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex); - // Update the custom order for both pages - customOrderPages[fromPage] = { order: fromPageOrder }; - customOrderPages[toPage] = { order: toPageOrder }; - } - } - - // Create a field update if it doesn't exist for this field yet - let fieldUpdate = {}; - if (hasValue(field)) { - fieldUpdate = pageState.fieldUpdates[field.uuid]; - if (hasNoValue(fieldUpdate)) { - fieldUpdate = { field: field, changeType: undefined } - } - } - - // Update the store's state with new values and return - return Object.assign({}, state, { [url]: Object.assign({}, pageState, { - fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}), - customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) }) - })}) -} - -/** - * Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within - * @param initialOrderPages The initial list of OrderPages - * @param customOrderPages The changed list of OrderPages - */ -function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) { - let changed = false; - initialOrderPages.forEach((orderPage: OrderPage, page: number) => { - if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) { - orderPage.order.forEach((id: string, index: number) => { - if (id !== customOrderPages[page].order[index]) { - changed = true; - return; - } - }); - if (changed) { - return; - } - } - }); - return changed; -} - -/** - * Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate - * @param initialPages The initial list of OrderPage objects - * @param order The list of UUIDs to create a page for - * @param pageSize The pageSize used to populate empty spacer pages - * @param page The index of the page to add - */ -function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] { - const result = [...initialPages]; - const orderPage: OrderPage = { order: order }; - if (page < result.length) { - // The page we're trying to add already exists in the list. Overwrite it. - result[page] = orderPage; - } else if (page === result.length) { - // The page we're trying to add is the next page in the list, add it. - result.push(orderPage); - } else { - // The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page. - const emptyOrder = []; - for (let i = 0; i < pageSize; i++) { - emptyOrder.push(undefined); - } - const emptyOrderPage: OrderPage = { order: emptyOrder }; - for (let i = result.length; i < page; i++) { - result.push(emptyOrderPage); - } - result.push(orderPage); - } - return result; -} diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 780a402a84..04018b8de2 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -2,7 +2,6 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; import { ObjectUpdatesService } from './object-updates.service'; import { - AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -13,8 +12,6 @@ import { Notification } from '../../../shared/notifications/models/notification. import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; import {Relationship} from '../../shared/item-relationships/relationship.model'; -import { MoveOperation } from 'fast-json-patch/lib/core'; -import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -47,7 +44,7 @@ describe('ObjectUpdatesService', () => { }; store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer()); + service = new ObjectUpdatesService(store); spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry)); spyOn(service as any, 'getFieldState').and.callFake((uuid) => { @@ -63,25 +60,6 @@ describe('ObjectUpdatesService', () => { }); }); - describe('initializeWithCustomOrder', () => { - const pageSize = 20; - const page = 0; - - it('should dispatch an INITIALIZE action with the correct URL, initial identifiables, last modified , custom order, page size and page', () => { - service.initializeWithCustomOrder(url, identifiables, modDate, pageSize, page); - expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate, identifiables.map((identifiable) => identifiable.uuid), pageSize, page)); - }); - }); - - describe('addPageToCustomOrder', () => { - const page = 2; - - it('should dispatch an ADD_PAGE_TO_CUSTOM_ORDER action with the correct URL, identifiables, custom order and page number to add', () => { - service.addPageToCustomOrder(url, identifiables, page); - expect(store.dispatch).toHaveBeenCalledWith(new AddPageToCustomOrderAction(url, identifiables, identifiables.map((identifiable) => identifiable.uuid), page)); - }); - }); - describe('getFieldUpdates', () => { it('should return the list of all fields, including their update if there is one', () => { const result$ = service.getFieldUpdates(url, identifiables); @@ -116,49 +94,6 @@ describe('ObjectUpdatesService', () => { }); }); - describe('getFieldUpdatesByCustomOrder', () => { - beforeEach(() => { - const fieldStates = { - [identifiable1.uuid]: { editable: false, isNew: false, isValid: true }, - [identifiable2.uuid]: { editable: true, isNew: false, isValid: false }, - [identifiable3.uuid]: { editable: true, isNew: true, isValid: true }, - }; - - const customOrder = { - initialOrderPages: [{ - order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] - }], - newOrderPages: [{ - order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid] - }], - pageSize: 20, - changed: true - }; - - const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder - }; - - (service as any).getObjectEntry.and.returnValue(observableOf(objectEntry)) - }); - - it('should return the list of all fields, including their update if there is one, ordered by their custom order', (done) => { - const result$ = service.getFieldUpdatesByCustomOrder(url, identifiables); - expect((service as any).getObjectEntry).toHaveBeenCalledWith(url); - - const expectedResult = { - [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, - [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, - [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE } - }; - - result$.subscribe((result) => { - expect(result).toEqual(expectedResult); - done(); - }); - }); - }); - describe('isEditable', () => { it('should return false if this identifiable is currently not editable in the store', () => { const result$ = service.isEditable(url, identifiable1.uuid); @@ -274,11 +209,7 @@ describe('ObjectUpdatesService', () => { }); describe('when updates are emtpy', () => { beforeEach(() => { - (service as any).getObjectEntry.and.returnValue(observableOf({ - customOrder: { - changed: false - } - })) + (service as any).getObjectEntry.and.returnValue(observableOf({})) }); it('should return false when there are no updates', () => { @@ -346,44 +277,4 @@ describe('ObjectUpdatesService', () => { }); }); - describe('getMoveOperations', () => { - beforeEach(() => { - const fieldStates = { - [identifiable1.uuid]: { editable: false, isNew: false, isValid: true }, - [identifiable2.uuid]: { editable: true, isNew: false, isValid: false }, - [identifiable3.uuid]: { editable: true, isNew: true, isValid: true }, - }; - - const customOrder = { - initialOrderPages: [{ - order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] - }], - newOrderPages: [{ - order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid] - }], - pageSize: 20, - changed: true - }; - - const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder - }; - - (service as any).getObjectEntry.and.returnValue(observableOf(objectEntry)) - }); - - it('should return the expected move operations', (done) => { - const result$ = service.getMoveOperations(url); - - const expectedResult = [ - { op: 'move', from: '/0', path: '/2' } - ] as MoveOperation[]; - - result$.subscribe((result) => { - expect(result).toEqual(expectedResult); - done(); - }); - }); - }); - }); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index c9a7f47e81..84f0f06035 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -8,16 +8,15 @@ import { Identifiable, OBJECT_UPDATES_TRASH_PATH, ObjectUpdatesEntry, - ObjectUpdatesState, OrderPage, + ObjectUpdatesState, VirtualMetadataSource } from './object-updates.reducer'; import { Observable } from 'rxjs'; import { - AddFieldUpdateAction, AddPageToCustomOrderAction, + AddFieldUpdateAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, - MoveFieldUpdateAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -25,11 +24,8 @@ import { SetValidFieldUpdateAction } from './object-updates.actions'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; -import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service'; -import { MoveOperation } from 'fast-json-patch/lib/core'; -import { flatten } from '@angular/compiler'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel */ @Injectable() export class ObjectUpdatesService { - constructor(private store: Store, - private comparator: ArrayMoveChangeAnalyzer) { - + constructor(private store: Store) { } /** @@ -67,28 +61,6 @@ export class ObjectUpdatesService { this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified)); } - /** - * Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored - * @param url The page's URL for which the changes are being mapped - * @param fields The initial fields for the page's object - * @param lastModified The date the object was last modified - * @param pageSize The page size to use for adding pages to the custom order - * @param page The first page to populate the custom order with - */ - initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void { - this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page)); - } - - /** - * Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking - * @param url The URL for which the changes are being mapped - * @param fields The fields to add a new page for - * @param page The page number (starting from index 0) - */ - addPageToCustomOrder(url, fields: Identifiable[], page: number): void { - this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page)); - } - /** * Method to dispatch an AddFieldUpdateAction to the store * @param url The page's URL for which the changes are saved @@ -153,7 +125,7 @@ export class ObjectUpdatesService { */ getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { const objectUpdates = this.getObjectEntry(url); - return objectUpdates.pipe(map((objectEntry) => { + return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => { const fieldUpdates: FieldUpdates = {}; for (const object of initialFields) { let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; @@ -166,31 +138,6 @@ export class ObjectUpdatesService { })) } - /** - * Method that combines the state's updates with the initial values (when there's no update), - * sorted by their custom order to create a FieldUpdates object - * @param url The URL of the page for which the FieldUpdates should be requested - * @param initialFields The initial values of the fields - * @param page The page to retrieve - */ - getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable { - const objectUpdates = this.getObjectEntry(url); - return objectUpdates.pipe(map((objectEntry) => { - const fieldUpdates: FieldUpdates = {}; - if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) { - for (const uuid of objectEntry.customOrder.newOrderPages[page].order) { - let fieldUpdate = objectEntry.fieldUpdates[uuid]; - if (isEmpty(fieldUpdate)) { - const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); - fieldUpdate = {field: identifiable, changeType: undefined}; - } - fieldUpdates[uuid] = fieldUpdate; - } - } - return fieldUpdates; - })) - } - /** * Method to check if a specific field is currently editable in the store * @param url The URL of the page on which the field resides @@ -260,19 +207,6 @@ export class ObjectUpdatesService { this.saveFieldUpdate(url, field, FieldChangeType.UPDATE); } - /** - * Dispatches a MoveFieldUpdateAction - * @param url The page's URL for which the changes are saved - * @param from The index of the object to move - * @param to The index to move the object to - * @param fromPage The page to move the object from - * @param toPage The page to move the object to - * @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages) - */ - saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) { - this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field)); - } - /** * Check whether the virtual metadata of a given item is selected to be saved as real metadata * @param url The URL of the page on which the field resides @@ -387,7 +321,7 @@ export class ObjectUpdatesService { * @param url The page's url to check for in the store */ hasUpdates(url: string): Observable { - return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed))); + return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates))); } /** @@ -405,19 +339,4 @@ export class ObjectUpdatesService { getLastModified(url: string): Observable { return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified)); } - - /** - * Get move operations based on the custom order - * @param url The page's url - */ - getMoveOperations(url: string): Observable { - return this.getObjectEntry(url).pipe( - map((objectEntry) => objectEntry.customOrder), - map((customOrder) => this.comparator.diff( - flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)), - flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order))) - ) - ); - } - } diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts deleted file mode 100644 index 6cc031f3c9..0000000000 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - RegistryBitstreamformatsSuccessResponse -} from '../cache/response.models'; -import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service'; - -describe('RegistryBitstreamformatsResponseParsingService', () => { - let service: RegistryBitstreamformatsResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - bitstreamformats: [ - { - uuid: 'uuid-1', - description: 'a description' - }, - { - uuid: 'uuid-2', - description: 'another description' - }, - ] - } - } - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts deleted file mode 100644 index 1cbcf358e3..0000000000 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@angular/core'; -import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - const bitstreamformats = payload._embedded.bitstreamformats; - payload.bitstreamformats = bitstreamformats; - - const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload); - return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page)); - } - -} diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts deleted file mode 100644 index 5ede21954a..0000000000 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - RegistryMetadatafieldsSuccessResponse -} from '../cache/response.models'; -import { RegistryMetadatafieldsResponseParsingService } from './registry-metadatafields-response-parsing.service'; - -describe('RegistryMetadatafieldsResponseParsingService', () => { - let service: RegistryMetadatafieldsResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - metadatafields: [ - { - id: 1, - element: 'element', - qualifier: 'qualifier', - scopeNote: 'a scope note', - _embedded: { - schema: { - id: 1, - prefix: 'test', - namespace: 'test namespace' - } - } - }, - { - id: 2, - element: 'secondelement', - qualifier: 'secondqualifier', - scopeNote: 'a second scope note', - _embedded: { - schema: { - id: 1, - prefix: 'test', - namespace: 'test namespace' - } - } - }, - ] - } - } - }) as DSpaceRESTV2Response; - - const emptyData = Object.assign({ - payload: {} - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryMetadatafieldsResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse); - }); - - it('should not produce an error and parse the data correctly when the data is empty', () => { - const response = service.parse(null, emptyData); - expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.ts deleted file mode 100644 index cf9484c4c4..0000000000 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core'; -import { hasValue } from '../../shared/empty.util'; -import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - let metadatafields = []; - - if (hasValue(payload._embedded)) { - metadatafields = payload._embedded.metadatafields; - metadatafields.forEach((field) => { - field.schema = field._embedded.schema; - }); - } - - payload.metadatafields = metadatafields; - - const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload); - return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); - } - -} diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts deleted file mode 100644 index e49305d06a..0000000000 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service'; -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { RegistryMetadataschemasSuccessResponse } from '../cache/response.models'; - -describe('RegistryMetadataschemasResponseParsingService', () => { - let service: RegistryMetadataschemasResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - metadataschemas: [ - { - id: 1, - prefix: 'test', - namespace: 'test namespace' - }, - { - id: 2, - prefix: 'second', - namespace: 'second test namespace' - } - ] - } - } - }) as DSpaceRESTV2Response; - - const emptyData = Object.assign({ - payload: {} - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryMetadataschemasResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse); - }); - - it('should not produce an error and parse the data correctly when the data is empty', () => { - const response = service.parse(null, emptyData); - expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts deleted file mode 100644 index 416ed19dc2..0000000000 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { hasValue } from '../../shared/empty.util'; -import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - let metadataschemas = []; - if (hasValue(payload._embedded)) { - metadataschemas = payload._embedded.metadataschemas; - } - payload.metadataschemas = metadataschemas; - - const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload); - return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); - } - -} diff --git a/src/app/core/data/relationship-type.service.spec.ts b/src/app/core/data/relationship-type.service.spec.ts index 0a86b4bc61..751d28bf90 100644 --- a/src/app/core/data/relationship-type.service.spec.ts +++ b/src/app/core/data/relationship-type.service.spec.ts @@ -1,8 +1,8 @@ import { of as observableOf } from 'rxjs'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 247dce1619..c8ce2c5c45 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -1,10 +1,11 @@ import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf } from 'rxjs/internal/observable/of'; import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils'; -import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { createSuccessfulRemoteDataObject$, spyOnOperator } from '../../shared/testing/utils'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { spyOnOperator } from '../../shared/testing/utils.test'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 4dde567c99..3d68e70206 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -21,7 +21,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; @@ -257,7 +257,7 @@ export class RelationshipService extends DataService { if (options) { findListOptions = Object.assign(new FindListOptions(), options); } - const searchParams = [new SearchParam('label', label), new SearchParam('dso', item.id)]; + const searchParams = [new RequestParam('label', label), new RequestParam('dso', item.id)]; if (findListOptions.searchParams) { findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; } else { diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index a9052aa8dc..7f793f4ac4 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -3,7 +3,6 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, of as observableOf } from 'rxjs'; import { catchError, filter, flatMap, map, take } from 'rxjs/operators'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { StoreActionTypes } from '../../store.actions'; import { getClassForType } from '../cache/builders/build-decorators'; @@ -22,11 +21,11 @@ import { RequestError, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; -export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) => +export const addToResponseCacheAndCompleteAction = (request: RestRequest) => (source: Observable): Observable => source.pipe( map((response: RestResponse) => { - return new RequestCompleteAction(request.uuid, response) + return new RequestCompleteAction(request.uuid, response); }) ); @@ -50,9 +49,9 @@ export class RequestEffects { } return this.restApi.request(request.method, request.href, body, request.options).pipe( map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)), - addToResponseCacheAndCompleteAction(request, this.EnvConfig), + addToResponseCacheAndCompleteAction(request), catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe( - addToResponseCacheAndCompleteAction(request, this.EnvConfig) + addToResponseCacheAndCompleteAction(request) )) ); }) @@ -72,7 +71,6 @@ export class RequestEffects { ); constructor( - @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, private actions$: Actions, private restApi: DSpaceRESTv2Service, private injector: Injector, diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 0655333502..b484a2ba4e 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -11,11 +11,9 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { RestRequestMethod } from './rest-request-method'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; -import { MetadataschemaParsingService } from './metadataschema-parsing.service'; -import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; @@ -146,7 +144,7 @@ export class FindListOptions { elementsPerPage?: number; currentPage?: number; sort?: SortOptions; - searchParams?: SearchParam[]; + searchParams?: RequestParam[]; startsWith?: string; } @@ -251,58 +249,6 @@ export class IntegrationRequest extends GetRequest { } } -/** - * Request to create a MetadataSchema - */ -export class CreateMetadataSchemaRequest extends PostRequest { - constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { - super(uuid, href, body, options); - } - - getResponseParser(): GenericConstructor { - return MetadataschemaParsingService; - } -} - -/** - * Request to update a MetadataSchema - */ -export class UpdateMetadataSchemaRequest extends PutRequest { - constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { - super(uuid, href, body, options); - } - - getResponseParser(): GenericConstructor { - return MetadataschemaParsingService; - } -} - -/** - * Request to create a MetadataField - */ -export class CreateMetadataFieldRequest extends PostRequest { - constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { - super(uuid, href, body, options); - } - - getResponseParser(): GenericConstructor { - return MetadatafieldParsingService; - } -} - -/** - * Request to update a MetadataField - */ -export class UpdateMetadataFieldRequest extends PutRequest { - constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { - super(uuid, href, body, options); - } - - getResponseParser(): GenericConstructor { - return MetadatafieldParsingService; - } -} - /** * Class representing a submission HTTP GET request object */ diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index cfb3611fc6..253577a701 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -4,8 +4,8 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { BehaviorSubject, EMPTY, of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; -import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; +import { defaultUUID, getMockUUIDService } from '../../shared/mocks/uuid.service.mock'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { UUIDService } from '../shared/uuid.service'; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 105d84cf4a..9a2c565301 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -201,8 +201,9 @@ export class RequestService { * Remove all request cache providing (part of) the href * This also includes href-to-uuid index cache * @param href A substring of the request(s) href + * @return Returns an observable emitting whether or not the cache is removed */ - removeByHrefSubstring(href: string) { + removeByHrefSubstring(href: string): Observable { this.store.pipe( select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)), take(1) @@ -213,6 +214,11 @@ export class RequestService { }); this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0); this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); + + return this.store.pipe( + select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)), + map((uuids) => isEmpty(uuids)) + ); } /** diff --git a/src/app/core/data/resource-policy.service.spec.ts b/src/app/core/data/resource-policy.service.spec.ts deleted file mode 100644 index abed805ca3..0000000000 --- a/src/app/core/data/resource-policy.service.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ResourcePolicy } from '../shared/resource-policy.model'; -import { RequestService } from './request.service'; -import { ResourcePolicyService } from './resource-policy.service'; - -describe('ResourcePolicyService', () => { - let scheduler: TestScheduler; - let service: ResourcePolicyService; - let requestService: RequestService; - let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; - const testObject = { - uuid: '664184ee-b254-45e8-970d-220e5ccc060b' - } as ResourcePolicy; - const requestURL = `https://rest.api/rest/api/resourcepolicies/${testObject.uuid}`; - const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; - - beforeEach(() => { - scheduler = getTestScheduler(); - - requestService = jasmine.createSpyObj('requestService', { - generateRequestId: requestUUID, - configure: true - }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: cold('a', { - a: { - payload: testObject - } - }) - }); - objectCache = {} as ObjectCacheService; - const halService = {} as HALEndpointService; - const notificationsService = {} as NotificationsService; - const http = {} as HttpClient; - const comparator = {} as any; - - service = new ResourcePolicyService( - requestService, - rdbService, - objectCache, - halService, - notificationsService, - http, - comparator - ); - - spyOn((service as any).dataService, 'findByHref').and.callThrough(); - }); - - describe('findByHref', () => { - it('should proxy the call to dataservice.findByHref', () => { - scheduler.schedule(() => service.findByHref(requestURL)); - scheduler.flush(); - - expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); - }); - - it('should return a RemoteData for the object with the given URL', () => { - const result = service.findByHref(requestURL); - const expected = cold('a', { - a: { - payload: testObject - } - }); - expect(result).toBeObservable(expected); - }); - }); -}); diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts deleted file mode 100644 index f66032925e..0000000000 --- a/src/app/core/data/resource-policy.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; - -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../cache/builders/build-decorators'; - -import { DataService } from '../data/data.service'; -import { RequestService } from '../data/request.service'; -import { FindListOptions } from '../data/request.models'; -import { Collection } from '../shared/collection.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ResourcePolicy } from '../shared/resource-policy.model'; -import { RemoteData } from '../data/remote-data'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RESOURCE_POLICY } from '../shared/resource-policy.resource-type'; -import { ChangeAnalyzer } from './change-analyzer'; -import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; - -/* tslint:disable:max-classes-per-file */ - -/** - * A private DataService implementation to delegate specific methods to. - */ -class DataServiceImpl extends DataService { - protected linkPath = 'resourcepolicies'; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: ChangeAnalyzer) { - super(); - } - -} - -/** - * A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint - */ -@Injectable() -@dataService(RESOURCE_POLICY) -export class ResourcePolicyService { - private dataService: DataServiceImpl; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); - } - - /** - * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} - * @param href The url of {@link ResourcePolicy} we want to retrieve - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findByHref(href: string, ...linksToFollow: Array>): Observable> { - return this.dataService.findByHref(href, ...linksToFollow); - } - - /** - * Returns a list of observables of {@link RemoteData} of {@link ResourcePolicy}s, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} - * @param href The url of the {@link ResourcePolicy} we want to retrieve - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); - } - - /** - * Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection} - * - * @param collection the {@link Collection} to retrieve the defaultAccessConditions for - * @param findListOptions the {@link FindListOptions} for the request - */ - getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable>> { - return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions); - } -} diff --git a/src/app/core/data/version-history-data.service.spec.ts b/src/app/core/data/version-history-data.service.spec.ts index 6728df71f1..1a9c402482 100644 --- a/src/app/core/data/version-history-data.service.spec.ts +++ b/src/app/core/data/version-history-data.service.spec.ts @@ -2,9 +2,9 @@ import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { VersionHistoryDataService } from './version-history-data.service'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { GetRequest } from './request.models'; const url = 'fake-url'; diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index cd7bc72884..0cb56f14a2 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -8,17 +8,8 @@ import { of as observableOf } from 'rxjs'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; import { TestScheduler } from 'rxjs/testing'; -import { - EPeopleRegistryCancelEPersonAction, - EPeopleRegistryEditEPersonAction -} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; -import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; -import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { SearchParam } from '../cache/models/search-param.model'; +import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; +import { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -31,6 +22,12 @@ import { Item } from '../shared/item.model'; import { PageInfo } from '../shared/page-info.model'; import { EPersonDataService } from './eperson-data.service'; import { EPerson } from './models/eperson.model'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; describe('EPersonDataService', () => { let service: EPersonDataService; @@ -82,7 +79,7 @@ describe('EPersonDataService', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), ], @@ -108,7 +105,7 @@ describe('EPersonDataService', () => { it('search by default scope (byMetadata) and no query', () => { service.searchByScope(null, ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -116,7 +113,7 @@ describe('EPersonDataService', () => { it('search metadata scope and no query', () => { service.searchByScope('metadata', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -124,7 +121,7 @@ describe('EPersonDataService', () => { it('search metadata scope and with query', () => { service.searchByScope('metadata', 'test'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', 'test'))] + searchParams: [Object.assign(new RequestParam('query', 'test'))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -132,7 +129,7 @@ describe('EPersonDataService', () => { it('search email scope and no query', () => { service.searchByScope('email', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('email', ''))] + searchParams: [Object.assign(new RequestParam('email', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byEmail', options); }); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index a8cee6f1de..86e53178a0 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -15,7 +15,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; @@ -97,7 +97,7 @@ export class EPersonDataService extends DataService { * @param linksToFollow */ private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - const searchParams = [new SearchParam('email', query)]; + const searchParams = [new RequestParam('email', query)]; return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow); } @@ -108,7 +108,7 @@ export class EPersonDataService extends DataService { * @param linksToFollow */ private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - const searchParams = [new SearchParam('query', query)]; + const searchParams = [new RequestParam('query', query)]; return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow); } @@ -119,7 +119,7 @@ export class EPersonDataService extends DataService { * @param options * @param linksToFollow */ - private getEPeopleBy(searchParams: SearchParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { let findListOptions = new FindListOptions(); if (options) { findListOptions = Object.assign(new FindListOptions(), options); diff --git a/src/app/core/eperson/eperson-response-parsing.service.ts b/src/app/core/eperson/eperson-response-parsing.service.ts index 76222323ae..e1270e130e 100644 --- a/src/app/core/eperson/eperson-response-parsing.service.ts +++ b/src/app/core/eperson/eperson-response-parsing.service.ts @@ -6,8 +6,6 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -20,7 +18,6 @@ export class EpersonResponseParsingService extends BaseResponseParsingService im protected toCache = false; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts index 138cf547f2..240e9d6805 100644 --- a/src/app/core/eperson/group-data.service.spec.ts +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -10,14 +10,8 @@ import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from '../../+admin/admin-access-control/group-registry/group-registry.actions'; -import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; -import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -28,6 +22,12 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { Item } from '../shared/item.model'; import { PageInfo } from '../shared/page-info.model'; import { GroupDataService } from './group-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; describe('GroupDataService', () => { let service: GroupDataService; @@ -63,7 +63,7 @@ describe('GroupDataService', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), ], @@ -103,7 +103,7 @@ describe('GroupDataService', () => { it('search with empty query', () => { service.searchGroups(''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -111,7 +111,7 @@ describe('GroupDataService', () => { it('search with query', () => { service.searchGroups('test'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', 'test'))] + searchParams: [Object.assign(new RequestParam('query', 'test'))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 574b4d997a..75f00310ec 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -14,7 +14,7 @@ import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; @@ -97,7 +97,7 @@ export class GroupDataService extends DataService { * @param linksToFollow */ public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - const searchParams = [new SearchParam('query', query)]; + const searchParams = [new RequestParam('query', query)]; let findListOptions = new FindListOptions(); if (options) { findListOptions = Object.assign(new FindListOptions(), options); @@ -121,7 +121,7 @@ export class GroupDataService extends DataService { isMemberOf(groupName: string): Observable { const searchHref = 'isMemberOf'; const options = new FindListOptions(); - options.searchParams = [new SearchParam('groupName', groupName)]; + options.searchParams = [new RequestParam('groupName', groupName)]; return this.searchBy(searchHref, options).pipe( filter((groups: RemoteData>) => !groups.isResponsePending), diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 8cc139744c..b5cb8c4dc4 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -1,5 +1,4 @@ import { Store } from '@ngrx/store'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models'; @@ -13,7 +12,6 @@ import { AuthorityValue } from './models/authority.value'; describe('IntegrationResponseParsingService', () => { let service: IntegrationResponseParsingService; - const EnvConfig = {} as GlobalConfig; const store = {} as Store; const objectCacheService = new ObjectCacheService(store, undefined); const name = 'type'; @@ -198,7 +196,7 @@ describe('IntegrationResponseParsingService', () => { } beforeEach(() => { initVars(); - service = new IntegrationResponseParsingService(EnvConfig, objectCacheService); + service = new IntegrationResponseParsingService(objectCacheService); }); describe('parse', () => { diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts index 0609cd804e..2719669bae 100644 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -6,8 +6,6 @@ import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cach import { isNotEmpty } from '../../shared/empty.util'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { IntegrationModel } from './models/integration.model'; import { AuthorityValue } from './models/authority.value'; @@ -19,7 +17,6 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic protected toCache = true; constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, ) { super(); diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index 02fff950ed..148a5df7b8 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -1,15 +1,15 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { RequestService } from '../data/request.service'; import { IntegrationRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { IntegrationService } from './integration.service'; import { IntegrationSearchOptions } from './models/integration-options.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; const LINK_NAME = 'authorities'; const ENTRIES = 'entries'; diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts index 4ecc215dc7..583b90a01f 100644 --- a/src/app/core/json-patch/json-patch-operations.service.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts @@ -5,12 +5,12 @@ import { TestScheduler } from 'rxjs/testing'; import { of as observableOf } from 'rxjs'; import { Store, StoreModule } from '@ngrx/store'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { RequestService } from '../data/request.service'; import { SubmissionPatchRequest } from '../data/request.models'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { JsonPatchOperationsService } from './json-patch-operations.service'; import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model'; import { CoreState } from '../core.reducers'; @@ -21,9 +21,10 @@ import { RollbacktPatchOperationsAction, StartTransactionPatchOperationsAction } from './json-patch-operations.actions'; -import { MockStore } from '../../shared/testing/mock-store'; +import { StoreMock } from '../../shared/testing/store.mock'; import { RequestEntry } from '../data/request.reducer'; import { catchError } from 'rxjs/operators'; +import { storeModuleConfig } from '../../app.reducer'; class TestService extends JsonPatchOperationsService { protected linkPath = ''; @@ -101,10 +102,10 @@ describe('JsonPatchOperationsService test suite', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), ], providers: [ - { provide: Store, useClass: MockStore } + { provide: Store, useClass: StoreMock } ] }).compileComponents(); })); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index e3f6c3401c..c21cf50512 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -11,9 +11,6 @@ import { Store, StoreModule } from '@ngrx/store'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { EmptyError } from 'rxjs/internal-compatibility'; -import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config'; - -import { GlobalConfig } from '../../../config/global-config.interface'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; @@ -23,11 +20,11 @@ import { MockBitstream2, MockBitstreamFormat1, MockBitstreamFormat2, - MockItem -} from '../../shared/mocks/mock-item'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; + ItemMock +} from '../../shared/mocks/item.mock'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { AuthService } from '../auth/auth.service'; import { BrowseService } from '../browse/browse.service'; @@ -53,6 +50,8 @@ import { PageInfo } from '../shared/page-info.model'; import { UUIDService } from '../shared/uuid.service'; import { MetadataService } from './metadata.service'; +import { environment } from '../../../environments/environment'; +import { storeModuleConfig } from '../../app.reducer'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -97,8 +96,6 @@ describe('MetadataService', () => { let tagStore: Map; - let envConfig: GlobalConfig; - beforeEach(() => { store = new Store(undefined, undefined, undefined); @@ -110,7 +107,7 @@ describe('MetadataService', () => { remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService); const mockBitstreamDataService = { findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - if (item.equals(MockItem)) { + if (item.equals(ItemMock)) { return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2])); } else { return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])); @@ -135,11 +132,11 @@ describe('MetadataService', () => { TestBed.configureTestingModule({ imports: [ CommonModule, - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), RouterTestingModule.withRoutes([ @@ -160,7 +157,6 @@ describe('MetadataService', () => { { provide: ObjectCacheService, useValue: objectCacheService }, { provide: RequestService, useValue: requestService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, - { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, { provide: HALEndpointService, useValue: {} }, { provide: AuthService, useValue: {} }, { provide: NotificationsService, useValue: {} }, @@ -184,8 +180,6 @@ describe('MetadataService', () => { metadataService = TestBed.get(MetadataService); authService = TestBed.get(AuthService); - envConfig = TestBed.get(GLOBAL_CONFIG); - router = TestBed.get(Router); location = TestBed.get(Location); @@ -195,7 +189,7 @@ describe('MetadataService', () => { }); it('items page should set meta tags', fakeAsync(() => { - spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem)); + spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(title.getTitle()).toEqual('Test PowerPoint Document'); @@ -208,24 +202,24 @@ describe('MetadataService', () => { })); it('items page should set meta tags as published Thesis', fakeAsync(() => { - spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(MockItem, 'Thesis')))); + spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis')))); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document'); expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher'); - expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([envConfig.ui.baseUrl, router.url].join('')); + expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join('')); expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content'); })); it('items page should set meta tags as published Technical Report', fakeAsync(() => { - spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(MockItem, 'Technical Report')))); + spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report')))); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); })); it('other navigation should title and description', fakeAsync(() => { - spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem)); + spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(tagStore.size).toBeGreaterThan(0); @@ -245,7 +239,7 @@ describe('MetadataService', () => { }); it('processRemoteData should not produce an EmptyError', fakeAsync(() => { - spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem)); + spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); spyOn(metadataService, 'processRemoteData').and.callThrough(); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); @@ -255,7 +249,7 @@ describe('MetadataService', () => { }); const mockRemoteData = (mockItem: Item): Observable> => { - return createSuccessfulRemoteDataObject$(MockItem); + return createSuccessfulRemoteDataObject$(ItemMock); }; const mockType = (mockItem: Item, type: string): Item => { diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index dbba9d83f6..02d2b0c86b 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -8,7 +8,6 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -21,6 +20,7 @@ import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators'; +import { environment } from '../../../environments/environment'; @Injectable() export class MetadataService { @@ -39,7 +39,6 @@ export class MetadataService { private dsoNameService: DSONameService, private bitstreamDataService: BitstreamDataService, private bitstreamFormatDataService: BitstreamFormatDataService, - @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig ) { // TODO: determine what open graph meta tags are needed and whether // the differ per route. potentially add image based on DSpaceObject @@ -255,7 +254,7 @@ export class MetadataService { */ private setCitationAbstractUrlTag(): void { if (this.currentObject.value instanceof Item) { - const value = [this.envConfig.ui.baseUrl, this.router.url].join(''); + const value = [environment.ui.baseUrl, this.router.url].join(''); this.addMetaTag('citation_abstract_html_url', value); } } @@ -272,7 +271,7 @@ export class MetadataService { first((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); - return [] + return []; })) .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { diff --git a/src/app/core/registry/registry-bitstreamformats-response.model.ts b/src/app/core/registry/registry-bitstreamformats-response.model.ts deleted file mode 100644 index 4da30b4ffc..0000000000 --- a/src/app/core/registry/registry-bitstreamformats-response.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { autoserialize, deserialize } from 'cerialize'; -import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; -import { HALLink } from '../shared/hal-link.model'; -import { PageInfo } from '../shared/page-info.model'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { link } from '../cache/builders/build-decorators'; - -export class RegistryBitstreamformatsResponse { - @autoserialize - page: PageInfo; - - /** - * The {@link HALLink}s for this RegistryBitstreamformatsResponse - */ - @deserialize - _links: { - self: HALLink; - bitstreamformats: HALLink; - }; - - @link(BITSTREAM_FORMAT) - bitstreamformats?: BitstreamFormat[]; - -} diff --git a/src/app/core/registry/registry-metadatafields-response.model.ts b/src/app/core/registry/registry-metadatafields-response.model.ts deleted file mode 100644 index 5dc492ab0f..0000000000 --- a/src/app/core/registry/registry-metadatafields-response.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { autoserialize, deserialize } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; -import { MetadataField } from '../metadata/metadata-field.model'; -import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; -import { HALLink } from '../shared/hal-link.model'; -import { PageInfo } from '../shared/page-info.model'; -import { ResourceType } from '../shared/resource-type'; -import { excludeFromEquals } from '../utilities/equals.decorators'; - -/** - * Class that represents a response with a registry's metadata fields - */ -@typedObject -export class RegistryMetadatafieldsResponse { - static type = METADATA_FIELD; - - /** - * The object type - */ - @excludeFromEquals - @autoserialize - type: ResourceType; - - /** - * List of metadata fields in the response - */ - @deserialize - metadatafields: MetadataField[]; - - /** - * Page info of this response - */ - @autoserialize - page: PageInfo; - - /** - * The REST link to this response - */ - @autoserialize - self: string; - - @deserialize - _links: { - self: HALLink, - } -} diff --git a/src/app/core/registry/registry-metadataschemas-response.model.ts b/src/app/core/registry/registry-metadataschemas-response.model.ts deleted file mode 100644 index 7a485d8849..0000000000 --- a/src/app/core/registry/registry-metadataschemas-response.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { autoserialize, deserialize } from 'cerialize'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; - -export class RegistryMetadataschemasResponse { - @deserialize - metadataschemas: MetadataSchema[]; - - @autoserialize - page: PageInfo; - - @autoserialize - self: string; -} diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index b466693649..203e58136c 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -3,8 +3,7 @@ import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; import { MetadataRegistryCancelFieldAction, MetadataRegistryCancelSchemaAction, @@ -17,29 +16,20 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { MockStore } from '../../shared/testing/mock-store'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { StoreMock } from '../../shared/testing/store.mock'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { - RegistryMetadatafieldsSuccessResponse, - RegistryMetadataschemasSuccessResponse, - RestResponse -} from '../cache/response.models'; -import { RemoteData } from '../data/remote-data'; -import { RequestEntry } from '../data/request.reducer'; -import { RequestService } from '../data/request.service'; +import { RestResponse } from '../cache/response.models'; import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { PageInfo } from '../shared/page-info.model'; -import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryService } from './registry.service'; +import { storeModuleConfig } from '../../app.reducer'; +import { FindListOptions } from '../data/request.models'; +import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; +import { MetadataFieldDataService } from '../data/metadata-field-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; @Component({ template: '' }) class DummyComponent { @@ -48,211 +38,169 @@ class DummyComponent { describe('RegistryService', () => { let registryService: RegistryService; let mockStore; - const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'registry-service-spec-pagination', - pageSize: 20 - }); + let metadataSchemaService: MetadataSchemaDataService; + let metadataFieldService: MetadataFieldDataService; - const mockSchemasList = [ - Object.assign(new MetadataSchema(), { - id: 1, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' } - }, - prefix: 'dc', - namespace: 'http://dublincore.org/documents/dcmi-terms/', - type: MetadataSchema.type - }), - Object.assign(new MetadataSchema(), { - id: 2, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' } - }, - prefix: 'mock', - namespace: 'http://dspace.org/mockschema', - type: MetadataSchema.type - }) - ]; - const mockFieldsList = [ - Object.assign(new MetadataField(), - { + let options: FindListOptions; + let mockSchemasList: MetadataSchema[]; + let mockFieldsList: MetadataField[]; + + function init() { + options = Object.assign(new FindListOptions(), { + currentPage: 1, + elementsPerPage: 20 + }); + + mockSchemasList = [ + Object.assign(new MetadataSchema(), { id: 1, _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' } + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' } }, - element: 'contributor', - qualifier: 'advisor', - scopeNote: null, - schema: mockSchemasList[0], - type: MetadataField.type + prefix: 'dc', + namespace: 'http://dublincore.org/documents/dcmi-terms/', + type: MetadataSchema.type }), - Object.assign(new MetadataField(), - { + Object.assign(new MetadataSchema(), { id: 2, _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' } + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' } }, - element: 'contributor', - qualifier: 'author', - scopeNote: null, - schema: mockSchemasList[0], - type: MetadataField.type - }), - Object.assign(new MetadataField(), - { - id: 3, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' } - }, - element: 'contributor', - qualifier: 'editor', - scopeNote: 'test scope note', - schema: mockSchemasList[1], - type: MetadataField.type - }), - Object.assign(new MetadataField(), - { - id: 4, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' } - }, - element: 'contributor', - qualifier: 'illustrator', - scopeNote: null, - schema: mockSchemasList[1], - type: MetadataField.type + prefix: 'mock', + namespace: 'http://dspace.org/mockschema', + type: MetadataSchema.type }) - ]; + ]; - const pageInfo = new PageInfo(); - pageInfo.elementsPerPage = 20; - pageInfo.currentPage = 1; - - const endpoint = 'path'; - const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`; - const fieldEndpointWithParams = `${endpoint}?schema=${mockSchemasList[0].prefix}&size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`; - - const halServiceStub = { - getEndpoint: (link: string) => observableOf(endpoint) - }; - - const rdbStub = { - toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { - return observableCombineLatest(requestEntryObs, - payloadObs).pipe(map(([req, pay]) => { - return { req, pay }; + mockFieldsList = [ + Object.assign(new MetadataField(), + { + id: 1, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' } + }, + element: 'contributor', + qualifier: 'advisor', + scopeNote: null, + schema: mockSchemasList[0], + type: MetadataField.type + }), + Object.assign(new MetadataField(), + { + id: 2, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' } + }, + element: 'contributor', + qualifier: 'author', + scopeNote: null, + schema: mockSchemasList[0], + type: MetadataField.type + }), + Object.assign(new MetadataField(), + { + id: 3, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' } + }, + element: 'contributor', + qualifier: 'editor', + scopeNote: 'test scope note', + schema: mockSchemasList[1], + type: MetadataField.type + }), + Object.assign(new MetadataField(), + { + id: 4, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' } + }, + element: 'contributor', + qualifier: 'illustrator', + scopeNote: null, + schema: mockSchemasList[1], + type: MetadataField.type }) - ); - }, - aggregate: (input: Array>>): Observable> => { - return createSuccessfulRemoteDataObject$([]); - } - }; + ]; + + metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)), + findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]), + createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), + deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')), + clearRequests: observableOf('href') + }); + + metadataFieldService = jasmine.createSpyObj('metadataFieldService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)), + findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]), + createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]), + deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')), + clearRequests: observableOf('href') + }); + } beforeEach(() => { + init(); TestBed.configureTestingModule({ - imports: [CommonModule, StoreModule.forRoot({}), TranslateModule.forRoot()], + imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()], declarations: [ DummyComponent ], providers: [ - { provide: RequestService, useValue: getMockRequestService() }, - { provide: RemoteDataBuildService, useValue: rdbStub }, - { provide: HALEndpointService, useValue: halServiceStub }, - { provide: Store, useClass: MockStore }, + { provide: Store, useClass: StoreMock }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: MetadataSchemaDataService, useValue: metadataSchemaService }, + { provide: MetadataFieldDataService, useValue: metadataFieldService }, RegistryService ] }); registryService = TestBed.get(RegistryService); mockStore = TestBed.get(Store); - spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint)); }); describe('when requesting metadataschemas', () => { - const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { - metadataschemas: mockSchemasList, - page: pageInfo - }); - const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + let result; beforeEach(() => { - (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); - /* tslint:disable:no-empty */ - registryService.getMetadataSchemas(pagination).subscribe((value) => { + result = registryService.getMetadataSchemas(options); + }); + + it('should call metadataSchemaService.findAll', (done) => { + result.subscribe(() => { + expect(metadataSchemaService.findAll).toHaveBeenCalled(); + done(); }); - /* tslint:enable:no-empty */ - }); - - it('should call getEndpoint on the halService', () => { - expect((registryService as any).halService.getEndpoint).toHaveBeenCalled(); - }); - - it('should send out the request on the request service', () => { - expect((registryService as any).requestService.configure).toHaveBeenCalled(); - }); - - it('should call getByHref on the request service with the correct request url', () => { - expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams); }); }); describe('when requesting metadataschema by name', () => { - const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { - metadataschemas: mockSchemasList, - page: pageInfo - }); - const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + let result; beforeEach(() => { - (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); - /* tslint:disable:no-empty */ - registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => { + result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix); + }); + + it('should call metadataSchemaService.findById with the correct ID', (done) => { + result.subscribe(() => { + expect(metadataSchemaService.findById).toHaveBeenCalledWith(`${mockSchemasList[0].id}`); + done(); }); - /* tslint:enable:no-empty */ - }); - - it('should call getEndpoint on the halService', () => { - expect((registryService as any).halService.getEndpoint).toHaveBeenCalled(); - }); - - it('should send out the request on the request service', () => { - expect((registryService as any).requestService.configure).toHaveBeenCalled(); - }); - - it('should call getByHref on the request service with the correct request url', () => { - expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint); }); }); describe('when requesting metadatafields', () => { - const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), { - metadatafields: mockFieldsList, - page: pageInfo - }); - const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + let result; beforeEach(() => { - (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); - /* tslint:disable:no-empty */ - registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => { + result = registryService.getAllMetadataFields(); + }); + + it('should call metadataFieldService.findAll', (done) => { + result.subscribe(() => { + expect(metadataFieldService.findAll).toHaveBeenCalled(); + done(); }); - /* tslint:enable:no-empty */ - }); - - it('should call getEndpoint on the halService', () => { - expect((registryService as any).halService.getEndpoint).toHaveBeenCalled(); - }); - - it('should send out the request on the request service', () => { - expect((registryService as any).requestService.configure).toHaveBeenCalled(); - }); - - it('should call getByHref on the request service with the correct request url', () => { - expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(fieldEndpointWithParams); }); }); @@ -369,9 +317,10 @@ describe('RegistryService', () => { result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]); }); - it('should return the created/updated metadata schema', () => { + it('should return the created/updated metadata schema', (done) => { result.subscribe((schema: MetadataSchema) => { expect(schema).toEqual(mockSchemasList[0]); + done(); }); }); }); @@ -383,9 +332,10 @@ describe('RegistryService', () => { result = registryService.createOrUpdateMetadataField(mockFieldsList[0]); }); - it('should return the created/updated metadata field', () => { + it('should return the created/updated metadata field', (done) => { result.subscribe((field: MetadataField) => { expect(field).toEqual(mockFieldsList[0]); + done(); }); }); }); @@ -424,7 +374,7 @@ describe('RegistryService', () => { }); it('should remove the requests related to metadata schemas from cache', () => { - expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled(); + expect(metadataSchemaService.clearRequests).toHaveBeenCalled(); }); }); @@ -434,7 +384,7 @@ describe('RegistryService', () => { }); it('should remove the requests related to metadata fields from cache', () => { - expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled(); + expect(metadataFieldService.clearRequests).toHaveBeenCalled(); }); }); }); diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index fbc42b26f4..79b982da8a 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -2,37 +2,18 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { PageInfo } from '../shared/page-info.model'; -import { - CreateMetadataFieldRequest, - CreateMetadataSchemaRequest, - DeleteRequest, - GetRequest, - RestRequest, - UpdateMetadataFieldRequest, - UpdateMetadataSchemaRequest -} from '../data/request.models'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { ResponseParsingService } from '../data/parsing.service'; -import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service'; +import { FindListOptions } from '../data/request.models'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; -import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, - RegistryMetadatafieldsSuccessResponse, - RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; -import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers'; @@ -48,15 +29,14 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators'; +import { flatMap, map, tap } from 'rxjs/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { HttpHeaders } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; -import { getClassForType } from '../cache/builders/build-decorators'; +import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; +import { MetadataFieldDataService } from '../data/metadata-field-data.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); @@ -70,221 +50,64 @@ const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelec @Injectable() export class RegistryService { - private metadataSchemasPath = 'metadataschemas'; - private metadataFieldsPath = 'metadatafields'; - - // private bitstreamFormatsPath = 'bitstreamformats'; - - constructor(protected requestService: RequestService, - private rdb: RemoteDataBuildService, - private halService: HALEndpointService, - private store: Store, + constructor(private store: Store, private notificationsService: NotificationsService, - private translateService: TranslateService) { + private translateService: TranslateService, + private metadataSchemaService: MetadataSchemaDataService, + private metadataFieldService: MetadataFieldDataService) { } /** * Retrieves all metadata schemas - * @param pagination The pagination info used to retrieve the schemas + * @param options The options used to retrieve the schemas + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataSchemas(pagination: PaginationComponentOptions): Observable>> { - const requestObs = this.getMetadataSchemasRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) - ); - - const metadataschemasObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe( - map(([metadataschemas, pageInfo]) => { - return new PaginatedList(pageInfo, metadataschemas); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); + public getMetadataSchemas(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.metadataSchemaService.findAll(options, ...linksToFollow); } /** * Retrieves a metadata schema by its name * @param schemaName The name of the schema to find + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataSchemaByName(schemaName: string): Observable> { - // Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema - const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'all-metadatafields-pagination', - pageSize: 10000 + public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array>): Observable> { + // Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema + const options: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 10000 }); - const requestObs = this.getMetadataSchemasRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + return this.getMetadataSchemas(options).pipe( + getFirstSucceededRemoteDataPayload(), + map((schemas: PaginatedList) => schemas.page), + isNotEmptyOperator(), + map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]), + flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow)) ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) - ); - - const metadataschemaObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas), - map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0]) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs); } /** * retrieves all metadata fields that belong to a certain metadata schema * @param schema The schema to filter by - * @param pagination The pagination info used to retrieve the fields + * @param options The options info used to retrieve the fields + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable>> { - const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) - ); - - const metadatafieldsObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - - map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe( - map(([metadatafields, pageInfo]) => { - return new PaginatedList(pageInfo, metadatafields); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); + public getMetadataFieldsBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow); } /** * Retrieve all existing metadata fields as a paginated list - * @param pagination Pagination options to determine which page of metadata fields should be requested - * When no pagination is provided, all metadata fields are requested in one large page + * @param options Options to determine which page of metadata fields should be requested + * When no options are provided, all metadata fields are requested in one large page + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @returns an observable that emits a remote data object with a page of metadata fields */ - public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable>> { - if (hasNoValue(pagination)) { - pagination = {currentPage: 1, pageSize: 10000} as any; + public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + if (hasNoValue(options)) { + options = {currentPage: 1, elementsPerPage: 10000} as any; } - const requestObs = this.getMetadataFieldsRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) - ); - - const metadatafieldsObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields), - /* Make sure to explicitly cast this into a MetadataField object, on first page loads this object comes from the object cache created by the server and its prototype is unknown */ - map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => Object.assign(new MetadataField(), metadataField))) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - - map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe( - map(([metadatafields, pageInfo]) => { - return new PaginatedList(pageInfo, metadatafields); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); - } - - public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable { - return this.halService.getEndpoint(this.metadataSchemasPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadataschemasResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - - private getMetadataFieldsBySchemaRequestObs(pagination: PaginationComponentOptions, schema: MetadataSchema): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath + '/search/bySchema').pipe( - // return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`schema=${schema.prefix}`); - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadatafieldsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - - private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadatafieldsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); + return this.metadataFieldService.findAll(options, ...linksToFollow); } public editMetadataSchema(schema: MetadataSchema) { @@ -386,59 +209,17 @@ export class RegistryService { * Create or Update a MetadataSchema * If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead * Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint): - * - On creation, a CreateMetadataSchemaRequest is used - * - On update, a UpdateMetadataSchemaRequest is used + * - On creation, a CreateRequest is used + * - On update, a PutRequest is used * @param schema The MetadataSchema to create or update */ public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable { const isUpdate = hasValue(schema.id); - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe( - isNotEmptyOperator(), - map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)), - distinctUntilChanged() - ); - - const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - if (isUpdate) { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'application/json'); - options.headers = headers; - return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options); - } else { - return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema)); - } - }) - ); - - // Execute the post/put request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - // Return created/updated schema - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful) { - if (hasValue((response as any).errorMessage)) { - this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); - } - } else { - this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); - return response; - } - }), - isNotEmptyOperator(), - map((response: MetadataschemaSuccessResponse) => { - if (isNotEmpty(response.metadataschema)) { - return response.metadataschema; - } + return this.metadataSchemaService.createOrUpdateMetadataSchema(schema).pipe( + getFirstSucceededRemoteDataPayload(), + hasValueOperator(), + tap(() => { + this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); }) ); } @@ -448,74 +229,32 @@ export class RegistryService { * @param id The id of the metadata schema to delete */ public deleteMetadataSchema(id: number): Observable { - return this.delete(this.metadataSchemasPath, id); + return this.metadataSchemaService.deleteAndReturnResponse(`${id}`); } /** * Method that clears a cached metadata schema request and returns its REST url */ public clearMetadataSchemaRequests(): Observable { - return this.halService.getEndpoint(this.metadataSchemasPath).pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ); + return this.metadataSchemaService.clearRequests(); } /** * Create or Update a MetadataField * If the MetadataField contains an id, it is assumed the field already exists and is updated instead * Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint): - * - On creation, a CreateMetadataFieldRequest is used - * - On update, a UpdateMetadataFieldRequest is used + * - On creation, a CreateRequest is used + * - On update, a PutRequest is used * @param field The MetadataField to create or update */ public createOrUpdateMetadataField(field: MetadataField): Observable { const isUpdate = hasValue(field.id); - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe( - isNotEmptyOperator(), - map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)), - distinctUntilChanged() - ); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - if (isUpdate) { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'application/json'); - options.headers = headers; - return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options); - } else { - return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field)); - } - }) - ); - - // Execute the post/put request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - // Return created/updated field - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful) { - if (hasValue((response as any).errorMessage)) { - this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); - } - } else { - const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`; - this.showNotifications(true, isUpdate, true, {field: fieldString}); - return response; - } - }), - isNotEmptyOperator(), - map((response: MetadatafieldSuccessResponse) => { - if (isNotEmpty(response.metadatafield)) { - return response.metadatafield; - } + return this.metadataFieldService.createOrUpdateMetadataField(field).pipe( + getFirstSucceededRemoteDataPayload(), + hasValueOperator(), + tap(() => { + const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`; + this.showNotifications(true, isUpdate, true, {field: fieldString}); }) ); } @@ -525,38 +264,13 @@ export class RegistryService { * @param id The id of the metadata field to delete */ public deleteMetadataField(id: number): Observable { - return this.delete(this.metadataFieldsPath, id); + return this.metadataFieldService.deleteAndReturnResponse(`${id}`); } /** * Method that clears a cached metadata field request and returns its REST url */ public clearMetadataFieldRequests(): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ); - } - - private delete(path: string, id: number): Observable { - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(path).pipe( - isNotEmptyOperator(), - map((endpoint: string) => `${endpoint}/${id}`), - distinctUntilChanged() - ); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => new DeleteRequest(requestId, endpoint)) - ); - - // Execute the delete request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() - ); + return this.metadataFieldService.clearRequests(); } private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) { diff --git a/src/app/core/cache/models/action-type.model.ts b/src/app/core/resource-policy/models/action-type.model.ts similarity index 75% rename from src/app/core/cache/models/action-type.model.ts rename to src/app/core/resource-policy/models/action-type.model.ts index 4965f93e89..93c69c3705 100644 --- a/src/app/core/cache/models/action-type.model.ts +++ b/src/app/core/resource-policy/models/action-type.model.ts @@ -5,27 +5,27 @@ export enum ActionType { /** * Action of reading, viewing or downloading something */ - READ = 0, + READ = 'READ', /** * Action of modifying something */ - WRITE = 1, + WRITE = 'WRITE', /** * Action of deleting something */ - DELETE = 2, + DELETE = 'DELETE', /** * Action of adding something to a container */ - ADD = 3, + ADD = 'ADD', /** * Action of removing something from a container */ - REMOVE = 4, + REMOVE = 'REMOVE', /** * Action of performing workflow step 1 @@ -50,15 +50,20 @@ export enum ActionType { /** * Default Read policies for Bitstreams submitted to container */ - DEFAULT_BITSTREAM_READ = 9, + DEFAULT_BITSTREAM_READ = 'DEFAULT_BITSTREAM_READ', /** * Default Read policies for Items submitted to container */ - DEFAULT_ITEM_READ = 10, + DEFAULT_ITEM_READ = 'DEFAULT_ITEM_READ', /** * Administrative actions */ - ADMIN = 11, + ADMIN = 'ADMIN', + + /** + * Action of withdrawn reading + */ + WITHDRAWN_READ = 'WITHDRAWN_READ' } diff --git a/src/app/core/resource-policy/models/policy-type.model.ts b/src/app/core/resource-policy/models/policy-type.model.ts new file mode 100644 index 0000000000..21193e5ce5 --- /dev/null +++ b/src/app/core/resource-policy/models/policy-type.model.ts @@ -0,0 +1,25 @@ +/** + * Enum representing the Policy Type of a Resource Policy + */ +export enum PolicyType { + /** + * A policy in place during the submission + */ + TYPE_SUBMISSION = 'TYPE_SUBMISSION', + + /** + * A policy in place during the approval workflow + */ + TYPE_WORKFLOW = 'TYPE_WORKFLOW', + + /** + * A policy that has been inherited from a container (the collection) + */ + TYPE_INHERITED = 'TYPE_INHERITED', + + /** + * A policy defined by the user during the submission or workflow phase + */ + TYPE_CUSTOM = 'TYPE_CUSTOM', + +} diff --git a/src/app/core/resource-policy/models/resource-policy.model.ts b/src/app/core/resource-policy/models/resource-policy.model.ts new file mode 100644 index 0000000000..27602557d6 --- /dev/null +++ b/src/app/core/resource-policy/models/resource-policy.model.ts @@ -0,0 +1,105 @@ +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { link, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { ActionType } from './action-type.model'; +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; +import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { PolicyType } from './policy-type.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../data/remote-data'; +import { GROUP } from '../../eperson/models/group.resource-type'; +import { Group } from '../../eperson/models/group.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { EPerson } from '../../eperson/models/eperson.model'; + +/** + * Model class for a Resource Policy + */ +@typedObject +export class ResourcePolicy implements CacheableObject { + static type = RESOURCE_POLICY; + + /** + * The identifier for this Resource Policy + */ + @autoserialize + id: string; + + /** + * The name for this Resource Policy + */ + @autoserialize + name: string; + + /** + * The description for this Resource Policy + */ + @autoserialize + description: string; + + /** + * The classification or this Resource Policy + */ + @autoserialize + policyType: PolicyType; + + /** + * The action that is allowed by this Resource Policy + */ + @autoserialize + action: ActionType; + + /** + * The first day of validity of the policy (format YYYY-MM-DD) + */ + @autoserialize + startDate: string; + + /** + * The last day of validity of the policy (format YYYY-MM-DD) + */ + @autoserialize + endDate: string; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The universally unique identifier for this Resource Policy + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. + */ + @deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') + uuid: string; + + /** + * The {@link HALLink}s for this ResourcePolicy + */ + @deserialize + _links: { + eperson: HALLink, + group: HALLink, + self: HALLink, + }; + + /** + * The eperson linked by this resource policy + * Will be undefined unless the version {@link HALLink} has been resolved. + */ + @link(EPERSON) + eperson?: Observable>; + + /** + * The group linked by this resource policy + * Will be undefined unless the version {@link HALLink} has been resolved. + */ + @link(GROUP) + group?: Observable>; +} diff --git a/src/app/core/shared/resource-policy.resource-type.ts b/src/app/core/resource-policy/models/resource-policy.resource-type.ts similarity index 52% rename from src/app/core/shared/resource-policy.resource-type.ts rename to src/app/core/resource-policy/models/resource-policy.resource-type.ts index 1811a3a0d1..d8ff3b9485 100644 --- a/src/app/core/shared/resource-policy.resource-type.ts +++ b/src/app/core/resource-policy/models/resource-policy.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from './resource-type'; +import { ResourceType } from '../../shared/resource-type'; /** * The resource type for ResourcePolicy @@ -6,4 +6,4 @@ import { ResourceType } from './resource-type'; * Needs to be in a separate file to prevent circular * dependencies in webpack. */ -export const RESOURCE_POLICY = new ResourceType('resourcePolicy'); +export const RESOURCE_POLICY = new ResourceType('resourcepolicy'); diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts new file mode 100644 index 0000000000..1c6ac47405 --- /dev/null +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -0,0 +1,319 @@ +import { HttpClient } from '@angular/common/http'; + +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from '../data/request.service'; +import { ResourcePolicyService } from './resource-policy.service'; +import { PolicyType } from './models/policy-type.model'; +import { ActionType } from './models/action-type.model'; +import { FindListOptions } from '../data/request.models'; +import { RequestParam } from '../cache/models/request-param.model'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedList } from '../data/paginated-list'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { RequestEntry } from '../data/request.reducer'; +import { RestResponse } from '../cache/response.models'; + +describe('ResourcePolicyService', () => { + let scheduler: TestScheduler; + let service: ResourcePolicyService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + } + }; + + const anotherResourcePolicy: any = { + id: '2', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.WRITE, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-2', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + } + }; + const endpointURL = `https://rest.api/rest/api/resourcepolicies`; + const requestURL = `https://rest.api/rest/api/resourcepolicies/${resourcePolicy.id}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + const resourcePolicyId = '1'; + const epersonUUID = '8b39g7ya-5a4b-438b-9686-be1d5b4a1c5a'; + const groupUUID = '8b39g7ya-5a4b-36987-9686-be1d5b4a1c5a'; + const resourceUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; + + const pageInfo = new PageInfo(); + const array = [resourcePolicy, anotherResourcePolicy]; + const paginatedList = new PaginatedList(pageInfo, array); + const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: resourcePolicyRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + }); + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + + service = new ResourcePolicyService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + comparator + ); + + spyOn((service as any).dataService, 'create').and.callThrough(); + spyOn((service as any).dataService, 'delete').and.callThrough(); + spyOn((service as any).dataService, 'update').and.callThrough(); + spyOn((service as any).dataService, 'findById').and.callThrough(); + spyOn((service as any).dataService, 'findByHref').and.callThrough(); + spyOn((service as any).dataService, 'searchBy').and.callThrough(); + spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(requestURL)); + }); + + describe('create', () => { + it('should proxy the call to dataservice.create with eperson UUID', () => { + scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID)); + const params = [ + new RequestParam('resource', resourceUUID), + new RequestParam('eperson', epersonUUID) + ]; + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params); + }); + + it('should proxy the call to dataservice.create with group UUID', () => { + scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, null, groupUUID)); + const params = [ + new RequestParam('resource', resourceUUID), + new RequestParam('group', groupUUID) + ]; + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.create(resourcePolicy, resourceUUID, epersonUUID); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('delete', () => { + it('should proxy the call to dataservice.create', () => { + scheduler.schedule(() => service.delete(resourcePolicyId)); + scheduler.flush(); + + expect((service as any).dataService.delete).toHaveBeenCalledWith(resourcePolicyId); + }); + }); + + describe('update', () => { + it('should proxy the call to dataservice.update', () => { + scheduler.schedule(() => service.update(resourcePolicy)); + scheduler.flush(); + + expect((service as any).dataService.update).toHaveBeenCalledWith(resourcePolicy); + }); + }); + + describe('findById', () => { + it('should proxy the call to dataservice.findById', () => { + scheduler.schedule(() => service.findById(resourcePolicyId)); + scheduler.flush(); + + expect((service as any).dataService.findById).toHaveBeenCalledWith(resourcePolicyId); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.findById(resourcePolicyId); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findByHref', () => { + it('should proxy the call to dataservice.findByHref', () => { + scheduler.schedule(() => service.findByHref(requestURL)); + scheduler.flush(); + + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findByHref(requestURL); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchByEPerson', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', epersonUUID)]; + scheduler.schedule(() => service.searchByEPerson(epersonUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const options = new FindListOptions(); + options.searchParams = [ + new RequestParam('uuid', epersonUUID), + new RequestParam('resource', resourceUUID), + ]; + scheduler.schedule(() => service.searchByEPerson(epersonUUID, resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByEPerson(epersonUUID, resourceUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('searchByGroup', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', groupUUID)]; + scheduler.schedule(() => service.searchByGroup(groupUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const options = new FindListOptions(); + options.searchParams = [ + new RequestParam('uuid', groupUUID), + new RequestParam('resource', resourceUUID), + ]; + scheduler.schedule(() => service.searchByGroup(groupUUID, resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByGroup(groupUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('searchByResource', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', resourceUUID)]; + scheduler.schedule(() => service.searchByResource(resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const action = ActionType.READ; + const options = new FindListOptions(); + options.searchParams = [ + new RequestParam('uuid', resourceUUID), + new RequestParam('action', action), + ]; + scheduler.schedule(() => service.searchByResource(resourceUUID, action)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByResource(resourceUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts new file mode 100644 index 0000000000..291920c35a --- /dev/null +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; + +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { FindListOptions } from '../data/request.models'; +import { Collection } from '../shared/collection.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ResourcePolicy } from './models/resource-policy.model'; +import { RemoteData } from '../data/remote-data'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RESOURCE_POLICY } from './models/resource-policy.resource-type'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { PaginatedList } from '../data/paginated-list'; +import { ActionType } from './models/action-type.model'; +import { RequestParam } from '../cache/models/request-param.model'; +import { isNotEmpty } from '../../shared/empty.util'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = 'resourcepolicies'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } + +} + +/** + * A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint + */ +@Injectable() +@dataService(RESOURCE_POLICY) +export class ResourcePolicyService { + private dataService: DataServiceImpl; + protected searchByEPersonMethod = 'eperson'; + protected searchByGroupMethod = 'group'; + protected searchByResourceMethod = 'resource'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Create a new ResourcePolicy on the server, and store the response + * in the object cache + * + * @param {ResourcePolicy} resourcePolicy + * The resource policy to create + * @param {string} resourceUUID + * The uuid of the resource target of the policy + * @param {string} epersonUUID + * The uuid of the eperson that will be grant of the permission. Exactly one of eperson or group is required + * @param {string} groupUUID + * The uuid of the group that will be grant of the permission. Exactly one of eperson or group is required + */ + create(resourcePolicy: ResourcePolicy, resourceUUID: string, epersonUUID?: string, groupUUID?: string): Observable> { + const params = []; + params.push(new RequestParam('resource', resourceUUID)); + if (isNotEmpty(epersonUUID)) { + params.push(new RequestParam('eperson', epersonUUID)); + } else if (isNotEmpty(groupUUID)) { + params.push(new RequestParam('group', groupUUID)); + } + return this.dataService.create(resourcePolicy, ...params); + } + + /** + * Delete an existing ResourcePolicy on the server + * + * @param resourcePolicyID The resource policy's id to be removed + * @return an observable that emits true when the deletion was successful, false when it failed + */ + delete(resourcePolicyID: string): Observable { + return this.dataService.delete(resourcePolicyID); + } + + /** + * Add a new patch to the object cache + * The patch is derived from the differences between the given object and its version in the object cache + * @param {ResourcePolicy} object The given object + */ + update(object: ResourcePolicy): Observable> { + return this.dataService.update(object); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} + * @param href The url of {@link ResourcePolicy} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on its ID, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param id ID of {@link ResourcePolicy} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findById(id: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findById(id, ...linksToFollow); + } + + /** + * Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection} + * + * @param collection the {@link Collection} to retrieve the defaultAccessConditions for + * @param findListOptions the {@link FindListOptions} for the request + */ + getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable>> { + return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions); + } + + /** + * Return the {@link ResourcePolicy} list for a {@link EPerson} + * + * @param UUID UUID of a given {@link EPerson} + * @param resourceUUID Limit the returned policies to the specified DSO + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', UUID)]; + if (isNotEmpty(resourceUUID)) { + options.searchParams.push(new RequestParam('resource', resourceUUID)) + } + return this.dataService.searchBy(this.searchByEPersonMethod, options, ...linksToFollow) + } + + /** + * Return the {@link ResourcePolicy} list for a {@link Group} + * + * @param UUID UUID of a given {@link Group} + * @param resourceUUID Limit the returned policies to the specified DSO + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', UUID)]; + if (isNotEmpty(resourceUUID)) { + options.searchParams.push(new RequestParam('resource', resourceUUID)) + } + return this.dataService.searchBy(this.searchByGroupMethod, options, ...linksToFollow) + } + + /** + * Return the {@link ResourcePolicy} list for a given DSO + * + * @param UUID UUID of a given DSO + * @param action Limit the returned policies to the specified {@link ActionType} + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array>): Observable>> { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', UUID)]; + if (isNotEmpty(action)) { + options.searchParams.push(new RequestParam('action', action)) + } + return this.dataService.searchBy(this.searchByResourceMethod, options, ...linksToFollow) + } + +} diff --git a/src/app/core/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts index 07ff56d879..1c92931b05 100644 --- a/src/app/core/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -6,7 +6,7 @@ import { Store } from '@ngrx/store'; import { getTestScheduler, hot } from 'jasmine-marbles'; import { RouteService } from './route.service'; -import { MockRouter } from '../../shared/mocks/mock-router'; +import { RouterMock } from '../../shared/mocks/router.mock'; import { TestScheduler } from 'rxjs/testing'; import { AddUrlToHistoryAction } from '../history/history.actions'; @@ -29,7 +29,7 @@ describe('RouteService', () => { select: jasmine.createSpy('select') }); - const router = new MockRouter(); + const router = new RouterMock(); router.setParams(convertToParamMap(paramObject)); paramObject[paramName1] = paramValue1; diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index c1164f0fc4..1e5c14d486 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,8 +1,15 @@ import { deserialize, inheritSerialization } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; + +import { Observable } from 'rxjs'; + +import { link, typedObject } from '../cache/builders/build-decorators'; import { BUNDLE } from './bundle.resource-type'; import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; +import { RemoteData } from '../data/remote-data'; +import { PaginatedList } from '../data/paginated-list'; +import { BITSTREAM } from './bitstream.resource-type'; +import { Bitstream } from './bitstream.model'; @typedObject @inheritSerialization(DSpaceObject) @@ -17,5 +24,19 @@ export class Bundle extends DSpaceObject { self: HALLink; primaryBitstream: HALLink; bitstreams: HALLink; - } + }; + + /** + * The primary Bitstream of this Bundle + * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. + */ + @link(BITSTREAM) + primaryBitstream?: Observable>; + + /** + * The list of Bitstreams that are direct children of this Bundle + * Will be undefined unless the bitstreams {@link HALLink} has been resolved. + */ + @link(BITSTREAM, true) + bitstreams?: Observable>>; } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 4e0b5ead83..b65ac252ef 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -10,8 +10,8 @@ import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; import { License } from './license.model'; import { LICENSE } from './license.resource-type'; -import { ResourcePolicy } from './resource-policy.model'; -import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { ResourcePolicy } from '../resource-policy/models/resource-policy.model'; +import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resource-type'; import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index 6bb3d77140..ff24b7d090 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -11,4 +11,5 @@ export enum Context { AdminMenu = 'adminMenu', SubmissionModal = 'submissionModal', AdminSearch = 'adminSearch', + AdminWorkflowSearch = 'adminWorkflowSearch', } diff --git a/src/app/core/shared/generic-constructor.ts b/src/app/core/shared/generic-constructor.ts index 095fbfcb7a..49a488dc9a 100644 --- a/src/app/core/shared/generic-constructor.ts +++ b/src/app/core/shared/generic-constructor.ts @@ -4,5 +4,5 @@ * https://github.com/Microsoft/TypeScript/issues/204#issuecomment-257722306 */ /* tslint:disable:interface-over-type-literal */ -export type GenericConstructor = { new(...args: any[]): T }; +export type GenericConstructor = new (...args: any[]) => T ; /* tslint:enable:interface-over-type-literal */ diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index cd03b6ec71..5f5c31f29d 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -1,18 +1,17 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from './hal-endpoint.service'; import { EndpointMapRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { of as observableOf } from 'rxjs'; +import { environment } from '../../../environments/environment'; describe('HALEndpointService', () => { let service: HALEndpointService; let requestService: RequestService; - let envConfig: GlobalConfig; let requestEntry; - + let envConfig; const endpointMap = { test: 'https://rest.api/test', foo: 'https://rest.api/foo', @@ -55,16 +54,14 @@ describe('HALEndpointService', () => { } as any; service = new HALEndpointService( - requestService, - envConfig + requestService ); }); describe('getRootEndpointMap', () => { - it('should configure a new EndpointMapRequest', () => { (service as any).getRootEndpointMap(); - const expected = new EndpointMapRequest(requestService.generateRequestId(), envConfig.rest.baseUrl); + const expected = new EndpointMapRequest(requestService.generateRequestId(), environment.rest.baseUrl); expect(requestService.configure).toHaveBeenCalledWith(expected); }); @@ -149,8 +146,7 @@ describe('HALEndpointService', () => { describe('isEnabledOnRestApi', () => { beforeEach(() => { service = new HALEndpointService( - requestService, - envConfig + requestService ); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 530ac086d1..d8b1d8931f 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,20 +1,10 @@ -import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; -import { - distinctUntilChanged, first, - map, - mergeMap, - startWith, - switchMap, take, - tap -} from 'rxjs/operators'; -import { RequestEntry } from '../data/request.reducer'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map, startWith, switchMap, take } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { EndpointMapRequest } from '../data/request.models'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; -import { Inject, Injectable } from '@angular/core'; -import { GLOBAL_CONFIG } from '../../../config'; +import { Injectable } from '@angular/core'; import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response.models'; import { getResponseFromEntry } from './operators'; import { URLCombiner } from '../url-combiner/url-combiner'; @@ -22,12 +12,11 @@ import { URLCombiner } from '../url-combiner/url-combiner'; @Injectable() export class HALEndpointService { - constructor(private requestService: RequestService, - @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { + constructor(private requestService: RequestService) { } protected getRootHref(): string { - return new RESTURLCombiner(this.EnvConfig, '/').toString(); + return new RESTURLCombiner('/').toString(); } protected getRootEndpointMap(): Observable { @@ -60,7 +49,7 @@ export class HALEndpointService { */ private getEndpointAt(href: string, ...halNames: string[]): Observable { if (isEmpty(halNames)) { - throw new Error('cant\'t fetch the URL without the HAL link names') + throw new Error('cant\'t fetch the URL without the HAL link names'); } const nextHref$ = this.getEndpointMapAt(href).pipe( @@ -91,7 +80,7 @@ export class HALEndpointService { map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[linkPath])), startWith(undefined), distinctUntilChanged() - ) + ); } } diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index 9a4e11e6fd..732ae5b19c 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -1,4 +1,5 @@ -import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from './item.model'; diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index ec069772f8..a19419259d 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -1,6 +1,6 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { GetRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; @@ -21,7 +21,7 @@ import { of as observableOf } from 'rxjs'; import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject -} from '../../shared/testing/utils'; +} from '../../shared/remote-data.utils'; describe('Core Module - RxJS Operators', () => { let scheduler: TestScheduler; diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index a51e711d26..a307b144d2 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -67,6 +67,10 @@ export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +export const getSucceededRemoteWithNotEmptyData = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload))); + /** * Get the first successful remotely retrieved object * @@ -84,6 +88,23 @@ export const getFirstSucceededRemoteDataPayload = () => getRemoteDataPayload() ); +/** + * Get the first successful remotely retrieved object with not empty payload + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => + (source: Observable>): Observable => + source.pipe( + getSucceededRemoteWithNotEmptyData(), + getRemoteDataPayload() + ); + /** * Get the all successful remotely retrieved objects * diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts deleted file mode 100644 index dd00a16e97..0000000000 --- a/src/app/core/shared/resource-policy.model.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { autoserialize, deserialize, deserializeAs } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; -import { IDToUUIDSerializer } from '../cache/id-to-uuid-serializer'; -import { ActionType } from '../cache/models/action-type.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { excludeFromEquals } from '../utilities/equals.decorators'; -import { HALLink } from './hal-link.model'; -import { RESOURCE_POLICY } from './resource-policy.resource-type'; -import { ResourceType } from './resource-type'; - -/** - * Model class for a Resource Policy - */ -@typedObject -export class ResourcePolicy implements CacheableObject { - static type = RESOURCE_POLICY; - - /** - * The object type - */ - @excludeFromEquals - @autoserialize - type: ResourceType; - - /** - * The action that is allowed by this Resource Policy - */ - @autoserialize - action: ActionType; - - /** - * The name for this Resource Policy - */ - @autoserialize - name: string; - - /** - * The uuid of the Group this Resource Policy applies to - */ - @autoserialize - groupUUID: string; - - /** - * The universally unique identifier for this Resource Policy - * This UUID is generated client-side and isn't used by the backend. - * It is based on the ID, so it will be the same for each refresh. - */ - @deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') - uuid: string; - - /** - * The {@link HALLink}s for this ResourcePolicy - */ - @deserialize - _links: { - self: HALLink, - } -} diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index b5423e0df0..ef275f3a50 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -1,5 +1,5 @@ import { SearchConfigurationService } from './search-configuration.service'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 8ae0855cae..c2bb5082c7 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -13,7 +13,7 @@ import { SortDirection, SortOptions } from '../../cache/models/sort-options.mode import { RouteService } from '../../services/route.service'; import { getSucceededRemoteData } from '../operators'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; /** * Service that performs all actions that have to do with the current search configuration diff --git a/src/app/core/shared/search/search-filter.service.spec.ts b/src/app/core/shared/search/search-filter.service.spec.ts index 91f53da898..04fa4fbce0 100644 --- a/src/app/core/shared/search/search-filter.service.spec.ts +++ b/src/app/core/shared/search/search-filter.service.spec.ts @@ -12,7 +12,7 @@ import { import { SearchFiltersState } from '../../../shared/search/search-filters/search-filter/search-filter.reducer'; import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model'; import { FilterType } from '../../../shared/search/filter-type.model'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { of as observableOf } from 'rxjs'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts index 0e093d119c..816d90cb2b 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -7,14 +7,14 @@ import { Component } from '@angular/core'; import { SearchService } from './search.service'; import { Router, UrlTree } from '@angular/router'; import { RequestService } from '../../data/request.service'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; -import { RouterStub } from '../../../shared/testing/router-stub'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { RouterStub } from '../../../shared/testing/router.stub'; import { HALEndpointService } from '../hal-endpoint.service'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { RemoteData } from '../../data/remote-data'; import { RequestEntry } from '../../data/request.reducer'; -import { getMockRequestService } from '../../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { FacetConfigSuccessResponse, SearchSuccessResponse } from '../../cache/response.models'; import { SearchQueryResponse } from '../../../shared/search/search-query-response.model'; import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model'; @@ -23,9 +23,9 @@ import { ViewMode } from '../view-mode.model'; import { DSpaceObjectDataService } from '../../data/dspace-object-data.service'; import { map } from 'rxjs/operators'; import { RouteService } from '../../services/route.service'; -import { routeServiceStub } from '../../../shared/testing/route-service-stub'; +import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; @Component({ template: '' }) class DummyComponent { diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts index 87ea19653d..5b0832975d 100644 --- a/src/app/core/submission/models/submission-object.model.ts +++ b/src/app/core/submission/models/submission-object.model.ts @@ -77,7 +77,9 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable * Will be undefined unless the item {@link HALLink} has been resolved. */ @link(ITEM) - item?: Observable> | Item; + /* This was changed from 'Observable> | Item' to 'any' to prevent issues in templates with async */ + item?: any; + /** * The configuration object that define this submission * Will be undefined unless the submissionDefinition {@link HALLink} has been resolved. diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index 27a7e43c46..afabde831a 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -9,8 +9,6 @@ import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util'; import { ConfigObject } from '../config/models/config.model'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; import { SubmissionObject } from './models/submission-object.model'; @@ -89,8 +87,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService */ protected shouldDirectlyAttachEmbeds = true; - constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService, + constructor(protected objectCache: ObjectCacheService, protected dsoParser: DSOResponseParsingService ) { super(); diff --git a/src/app/core/submission/submission-rest.service.spec.ts b/src/app/core/submission/submission-rest.service.spec.ts index 68d7ff13f4..e57a350e55 100644 --- a/src/app/core/submission/submission-rest.service.spec.ts +++ b/src/app/core/submission/submission-rest.service.spec.ts @@ -4,9 +4,9 @@ import { getTestScheduler } from 'jasmine-marbles'; import { SubmissionRestService } from './submission-rest.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { SubmissionDeleteRequest, SubmissionPatchRequest, diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index a2dfca5eb3..c82f7bf0b5 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -9,13 +9,17 @@ import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { WorkflowItem } from './models/workflowitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindListOptions } from '../data/request.models'; +import { DeleteByIDRequest, FindListOptions } from '../data/request.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { Observable } from 'rxjs'; +import { find, map } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { RequestEntry } from '../data/request.reducer'; /** - * A service that provides methods to make REST requests with workflowitems endpoint. + * A service that provides methods to make REST requests with workflow items endpoint. */ @Injectable() @dataService(WorkflowItem.type) @@ -35,4 +39,50 @@ export class WorkflowItemDataService extends DataService { super(); } + /** + * Delete an existing Workflow Item on the server + * @param id The Workflow Item's id to be removed + * @return an observable that emits true when the deletion was successful, false when it failed + */ + delete(id: string): Observable { + return this.deleteWFI(id, true) + } + + /** + * Send an existing Workflow Item back to the workflow on the server + * @param id The Workspace Item's id to be sent back + * @return an observable that emits true when sending back the item was successful, false when it failed + */ + sendBack(id: string): Observable { + return this.deleteWFI(id, false) + } + + /** + * Method to delete a workflow item from the server + * @param id The identifier of the server + * @param expunge Whether or not to expunge: + * When true, the workflow item and its item will be permanently expunged on the server + * When false, the workflow item will be removed, but the item will still be available as a workspace item + */ + private deleteWFI(id: string, expunge: boolean): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, id)), + map((endpoint: string) => endpoint + '?expunge=' + expunge) + ); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, id); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } } diff --git a/src/app/core/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts index 078fe1e63f..c787d01282 100644 --- a/src/app/core/tasks/claimed-task-data.service.spec.ts +++ b/src/app/core/tasks/claimed-task-data.service.spec.ts @@ -2,8 +2,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CoreState } from '../core.reducers'; diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 86e0b46f36..d90c7a19c0 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -67,7 +67,8 @@ export class TaskObject extends DSpaceObject implements CacheableObject { * Will be undefined unless the workflowitem {@link HALLink} has been resolved. */ @link(WORKFLOWITEM) - workflowitem?: Observable> | WorkflowItem; + /* This was changed from 'WorkflowItem | Observable>' to 'any' to prevent issues in templates with async */ + workflowitem?: any; /** * The task action type diff --git a/src/app/core/tasks/pool-task-data.service.spec.ts b/src/app/core/tasks/pool-task-data.service.spec.ts index 70ae4c7a91..e8511aca6f 100644 --- a/src/app/core/tasks/pool-task-data.service.spec.ts +++ b/src/app/core/tasks/pool-task-data.service.spec.ts @@ -2,8 +2,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CoreState } from '../core.reducers'; diff --git a/src/app/core/tasks/task-response-parsing.service.ts b/src/app/core/tasks/task-response-parsing.service.ts index 090b67ccbb..c4bf8afb05 100644 --- a/src/app/core/tasks/task-response-parsing.service.ts +++ b/src/app/core/tasks/task-response-parsing.service.ts @@ -5,8 +5,6 @@ import { RestRequest } from '../data/request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, RestResponse, TaskResponse } from '../cache/response.models'; @@ -21,11 +19,9 @@ export class TaskResponseParsingService extends BaseResponseParsingService imple /** * Initialize instance variables * - * @param {GlobalConfig} EnvConfig * @param {ObjectCacheService} objectCache */ - constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService,) { + constructor(protected objectCache: ObjectCacheService) { super(); } diff --git a/src/app/core/tasks/tasks.service.spec.ts b/src/app/core/tasks/tasks.service.spec.ts index 782a950b2d..9c8750a832 100644 --- a/src/app/core/tasks/tasks.service.spec.ts +++ b/src/app/core/tasks/tasks.service.spec.ts @@ -1,12 +1,12 @@ import { getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { TasksService } from './tasks.service'; import { RequestService } from '../data/request.service'; import { TaskDeleteRequest, TaskPostRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { TaskObject } from './models/task-object.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { Store } from '@ngrx/store'; diff --git a/src/app/core/url-combiner/rest-url-combiner.ts b/src/app/core/url-combiner/rest-url-combiner.ts index 06561c2379..92134050b5 100644 --- a/src/app/core/url-combiner/rest-url-combiner.ts +++ b/src/app/core/url-combiner/rest-url-combiner.ts @@ -1,6 +1,5 @@ import { URLCombiner } from './url-combiner'; - -import { GlobalConfig } from '../../../config'; +import { environment } from '../../../environments/environment'; /** * Combines a variable number of strings representing parts @@ -9,7 +8,7 @@ import { GlobalConfig } from '../../../config'; * TODO write tests once GlobalConfig becomes injectable */ export class RESTURLCombiner extends URLCombiner { - constructor(EnvConfig: GlobalConfig, ...parts: string[]) { - super(EnvConfig.rest.baseUrl, ...parts); + constructor(...parts: string[]) { + super(environment.rest.baseUrl, ...parts); } } diff --git a/src/app/core/url-combiner/ui-url-combiner.ts b/src/app/core/url-combiner/ui-url-combiner.ts index 2ebb26bb76..534c5620b5 100644 --- a/src/app/core/url-combiner/ui-url-combiner.ts +++ b/src/app/core/url-combiner/ui-url-combiner.ts @@ -1,5 +1,5 @@ import { URLCombiner } from './url-combiner'; -import { GlobalConfig } from '../../../config'; +import { environment } from '../../../environments/environment'; /** * Combines a variable number of strings representing parts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config'; * TODO write tests once GlobalConfig becomes injectable */ export class UIURLCombiner extends URLCombiner { - constructor(EnvConfig: GlobalConfig, ...parts: string[]) { - super(EnvConfig.ui.baseUrl, ...parts); + constructor(...parts: string[]) { + super(environment.ui.baseUrl, ...parts); } } diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts index 854ca1065d..31614158dd 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { Item } from '../../../../core/shared/item.model'; import { JournalIssueGridElementComponent } from './journal-issue-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts index 7405bb7ab4..81cb6ff215 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { Item } from '../../../../core/shared/item.model'; import { JournalVolumeGridElementComponent } from './journal-volume-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts index 7355c4aad1..db6d701a7e 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts @@ -1,7 +1,7 @@ import { Item } from '../../../../core/shared/item.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { JournalGridElementComponent } from './journal-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { async, TestBed } from '@angular/core/testing'; diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.spec.ts index 98616daa0b..f65fd97cb0 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { getEntityGridElementTestComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec'; diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.spec.ts index 9dfa3c3ab3..a7d5acdd00 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { getEntityGridElementTestComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec'; diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.spec.ts index 4aea6ef156..180b7f4600 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { getEntityGridElementTestComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec'; diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts index 64c1fc3bf7..2ceafa7372 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts @@ -3,7 +3,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { JournalIssueComponent } from './journal-issue.component'; import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts index f679d80ce7..7849a7e077 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts @@ -8,7 +8,7 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 3cd2ac1cce..91373ce3e0 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -22,9 +22,9 @@ import { Item } from '../../../../core/shared/item.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { isNotEmpty } from '../../../../shared/empty.util'; -import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { JournalComponent } from './journal.component'; @@ -67,7 +67,7 @@ describe('JournalComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe], diff --git a/src/app/entity-groups/research-entities/item-grid-elements/org-unit/org-unit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/org-unit/org-unit-grid-element.component.spec.ts index f5a0b04dba..1945f67a5c 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/org-unit/org-unit-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/org-unit/org-unit-grid-element.component.spec.ts @@ -1,7 +1,7 @@ import { Item } from '../../../../core/shared/item.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { OrgUnitGridElementComponent } from './org-unit-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { async, TestBed } from '@angular/core/testing'; diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts index 469f45ebf5..99001e887f 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts @@ -1,7 +1,7 @@ import { Item } from '../../../../core/shared/item.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { PersonGridElementComponent } from './person-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { async, TestBed } from '@angular/core/testing'; diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts index ff9e3bb64a..c2ca0a303e 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts @@ -1,7 +1,7 @@ import { Item } from '../../../../core/shared/item.model'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { ProjectGridElementComponent } from './project-grid-element.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { async, TestBed } from '@angular/core/testing'; diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.spec.ts index a44d2d1373..8dec83295e 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { OrgUnitSearchResultGridElementComponent } from './org-unit-search-result-grid-element.component'; diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts index 4938af2b73..f56d6c76af 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { getEntityGridElementTestComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec'; diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.spec.ts index 6497fadbaf..5a25eea955 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.spec.ts @@ -1,6 +1,6 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { ProjectSearchResultGridElementComponent } from './project-search-result-grid-element.component'; diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.spec.ts index 8bfedbed21..2582754b2f 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.spec.ts @@ -8,7 +8,7 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts index 8954d27de8..471444de64 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts @@ -5,7 +5,7 @@ import { import { PaginatedList } from '../../../../core/data/paginated-list'; import { Item } from '../../../../core/shared/item.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { PersonComponent } from './person.component'; const mockItem: Item = Object.assign(new Item(), { diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts index 72857654ce..24dc865e2f 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts @@ -5,7 +5,7 @@ import { import { PaginatedList } from '../../../../core/data/paginated-list'; import { Item } from '../../../../core/shared/item.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { ProjectComponent } from './project.component'; const mockItem: Item = Object.assign(new Item(), { diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss index 8fc6d2138d..78cc32591b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss @@ -1,7 +1,5 @@ -@import '../../../../../../styles/variables'; - $submission-relationship-thumbnail-width: 80px; .person-thumbnail { width: $submission-relationship-thumbnail-width; -} \ No newline at end of file +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts index e1520d9edd..52f5de92d7 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts @@ -24,7 +24,7 @@ import { UUIDService } from '../../../../../core/shared/uuid.service'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { OrgUnitSearchResultListSubmissionElementComponent } from './org-unit-search-result-list-submission-element.component'; diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss index 8fc6d2138d..78cc32591b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss @@ -1,7 +1,5 @@ -@import '../../../../../../styles/variables'; - $submission-relationship-thumbnail-width: 80px; .person-thumbnail { width: $submission-relationship-thumbnail-width; -} \ No newline at end of file +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts index 0949ebea7e..8064e9be79 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts @@ -24,7 +24,7 @@ import { UUIDService } from '../../../../../core/shared/uuid.service'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component'; diff --git a/src/app/footer/footer.component.spec.ts b/src/app/footer/footer.component.spec.ts index dde432e1ef..8d1d3a9891 100644 --- a/src/app/footer/footer.component.spec.ts +++ b/src/app/footer/footer.component.spec.ts @@ -21,7 +21,8 @@ import { Store, StoreModule } from '@ngrx/store'; // Load the implementations that should be tested import { FooterComponent } from './footer.component'; -import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; +import { storeModuleConfig } from '../app.reducer'; let comp: FooterComponent; let fixture: ComponentFixture; @@ -33,10 +34,10 @@ describe('Footer component', () => { // async beforeEach beforeEach(async(() => { return TestBed.configureTestingModule({ - imports: [CommonModule, StoreModule.forRoot({}), TranslateModule.forRoot({ + imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [FooterComponent], // declare the test component diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 58f7cb1ecf..5ce0cdb410 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -8,6 +8,7 @@ +
+ + +
+
\ No newline at end of file diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.scss b/src/app/shared/collection-dropdown/collection-dropdown.component.scss new file mode 100644 index 0000000000..deecc39510 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.scss @@ -0,0 +1,15 @@ +.scrollable-menu { + height: auto; + max-height: $dropdown-menu-max-height; + overflow-x: hidden; +} + +.collection-item { + border-bottom: $dropdown-border-width solid $dropdown-border-color; +} + +#collectionControlsDropdownMenu { + outline: 0; + left: 0 !important; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts new file mode 100644 index 0000000000..8530be665e --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts @@ -0,0 +1,241 @@ +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { CollectionDropdownComponent } from './collection-dropdown.component'; +import { FollowLinkConfig } from '../utils/follow-link-config.model'; +import { Observable, of } from 'rxjs'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { PageInfo } from 'src/app/core/shared/page-info.model'; +import { Collection } from '../../core/shared/collection.model'; +import { NO_ERRORS_SCHEMA, ChangeDetectorRef, ElementRef } from '@angular/core'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; +import { TestScheduler } from 'rxjs/testing'; +import { By } from '@angular/platform-browser'; +import { Community } from 'src/app/core/shared/community.model'; + +const community: Community = Object.assign(new Community(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Community 1' +}); + +const collections: Collection[] = [ + Object.assign(new Collection(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Collection 1', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + name: 'Collection 2', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 2' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 3', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 3' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + name: 'Collection 4', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 4' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'a5159760-f362-4659-9e81-e3253ad91ede', + name: 'Collection 5', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 5' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }) +]; + +const listElementMock = { + communities: [ + { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Community 1' + } + ], + collection: { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 3' + } + }; + +// tslint:disable-next-line: max-classes-per-file +class CollectionDataServiceMock { + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return of( + createSuccessfulRemoteDataObject( + new PaginatedList(new PageInfo(), collections) + ) + ); + } +} + +describe('CollectionDropdownComponent', () => { + let component: CollectionDropdownComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + const searchedCollection = 'TEXT'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ CollectionDropdownComponent ], + providers: [ + {provide: CollectionDataService, useClass: CollectionDataServiceMock}, + {provide: ChangeDetectorRef, useValue: {}}, + {provide: ElementRef, userValue: {}} + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(CollectionDropdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should populate collections list with five items', () => { + const elements = fixture.debugElement.queryAll(By.css('.collection-item')); + expect(elements.length).toEqual(5); + }); + + it('should trigger onSelect method when select a new collection from list', fakeAsync(() => { + spyOn(component, 'onSelect'); + const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)')); + collectionItem.triggerEventHandler('click', null); + fixture.detectChanges(); + tick(); + fixture.whenStable().then(() => { + expect(component.onSelect).toHaveBeenCalled(); + }); + })); + + it('should init component with collection list', fakeAsync(() => { + spyOn(component.subs, 'push').and.callThrough(); + spyOn(component, 'resetPagination').and.callThrough(); + spyOn(component, 'populateCollectionList').and.callThrough(); + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(component.subs.push).toHaveBeenCalled(); + expect(component.resetPagination).toHaveBeenCalled(); + expect(component.populateCollectionList).toHaveBeenCalled(); + }); + })); + + it('should emit collectionChange event when selecting a new collection', () => { + spyOn(component.selectionChange, 'emit').and.callThrough(); + component.ngOnInit(); + component.onSelect(listElementMock as any); + fixture.detectChanges(); + + expect(component.selectionChange.emit).toHaveBeenCalledWith(listElementMock as any); + }); + + it('should reset collections list after reset of searchField', fakeAsync(() => { + spyOn(component.subs, 'push').and.callThrough(); + spyOn(component, 'reset').and.callThrough(); + spyOn(component.searchField, 'setValue').and.callThrough(); + spyOn(component, 'resetPagination').and.callThrough(); + spyOn(component, 'populateCollectionList').and.callThrough(); + component.reset(); + const input = fixture.debugElement.query(By.css('input.form-control')); + const el = input.nativeElement; + el.value = searchedCollection; + el.dispatchEvent(new Event('input')); + fixture.detectChanges(); + tick(500); + + fixture.whenStable().then(() => { + expect(component.reset).toHaveBeenCalled(); + expect(component.searchField.setValue).toHaveBeenCalledWith(''); + expect(component.resetPagination).toHaveBeenCalled(); + expect(component.currentQuery).toEqual(''); + expect(component.populateCollectionList).toHaveBeenCalledWith(component.currentQuery, component.currentPage); + expect(component.searchListCollection).toEqual(collections as any); + expect(component.subs.push).toHaveBeenCalled(); + }); + })); + + it('should reset searchField when dropdown menu has been closed', () => { + spyOn(component.searchField, 'setValue').and.callThrough(); + component.reset(); + + expect(component.searchField.setValue).toHaveBeenCalled(); + }); + + it('should change loader status', () => { + spyOn(component.isLoadingList, 'next').and.callThrough(); + component.hideShowLoader(true); + + expect(component.isLoadingList.next).toHaveBeenCalledWith(true); + }); + + it('reset pagination fields', () => { + component.resetPagination(); + + expect(component.currentPage).toEqual(1); + expect(component.currentQuery).toEqual(''); + expect(component.hasNextPage).toEqual(true); + expect(component.searchListCollection).toEqual([]); + }); +}); diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts new file mode 100644 index 0000000000..05105d74a7 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -0,0 +1,236 @@ +import { Component, OnInit, HostListener, ChangeDetectorRef, OnDestroy, Output, EventEmitter, ElementRef } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Observable, Subscription, BehaviorSubject } from 'rxjs'; +import { hasValue } from '../empty.util'; +import { map, mergeMap, startWith, debounceTime, distinctUntilChanged, switchMap, reduce } from 'rxjs/operators'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { Community } from 'src/app/core/shared/community.model'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { followLink } from '../utils/follow-link-config.model'; +import { getFirstSucceededRemoteDataPayload, getSucceededRemoteWithNotEmptyData } from '../../core/shared/operators'; + +/** + * An interface to represent a collection entry + */ +interface CollectionListEntryItem { + id: string; + uuid: string; + name: string; +} + +/** + * An interface to represent an entry in the collection list + */ +interface CollectionListEntry { + communities: CollectionListEntryItem[], + collection: CollectionListEntryItem +} + +@Component({ + selector: 'ds-collection-dropdown', + templateUrl: './collection-dropdown.component.html', + styleUrls: ['./collection-dropdown.component.scss'] +}) +export class CollectionDropdownComponent implements OnInit, OnDestroy { + + /** + * The search form control + * @type {FormControl} + */ + public searchField: FormControl = new FormControl(); + + /** + * The collection list obtained from a search + * @type {Observable} + */ + public searchListCollection$: Observable; + + /** + * A boolean representing if dropdown list is scrollable to the bottom + * @type {boolean} + */ + private scrollableBottom = false; + + /** + * A boolean representing if dropdown list is scrollable to the top + * @type {boolean} + */ + private scrollableTop = false; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + /** + * The list of collection to render + */ + searchListCollection: CollectionListEntry[] = []; + + @Output() selectionChange = new EventEmitter(); + /** + * A boolean representing if the loader is visible or not + */ + isLoadingList: BehaviorSubject = new BehaviorSubject(false); + + /** + * A numeric representig current page + */ + currentPage: number; + + /** + * A boolean representing if exist another page to render + */ + hasNextPage: boolean; + + /** + * Current seach query used to filter collection list + */ + currentQuery: string; + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private collectionDataService: CollectionDataService, + private el: ElementRef + ) { } + + /** + * Method called on mousewheel event, it prevent the page scroll + * when arriving at the top/bottom of dropdown menu + * + * @param event + * mousewheel event + */ + @HostListener('mousewheel', ['$event']) onMousewheel(event) { + if (event.wheelDelta > 0 && this.scrollableTop) { + event.preventDefault(); + } + if (event.wheelDelta < 0 && this.scrollableBottom) { + event.preventDefault(); + } + } + + /** + * Initialize collection list + */ + ngOnInit() { + this.subs.push(this.searchField.valueChanges.pipe( + debounceTime(500), + distinctUntilChanged(), + startWith('') + ).subscribe( + (next) => { + if (hasValue(next) && next !== this.currentQuery) { + this.resetPagination(); + this.currentQuery = next; + this.populateCollectionList(this.currentQuery, this.currentPage); + } + } + )); + // Workaround for prevent the scroll of main page when this component is placed in a dialog + setTimeout(() => this.el.nativeElement.querySelector('input').focus(), 0); + } + + /** + * Check if dropdown scrollbar is at the top or bottom of the dropdown list + * + * @param event + */ + onScroll(event) { + this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight); + this.scrollableTop = (event.target.scrollTop === 0); + } + + /** + * Method used from infitity scroll for retrive more data on scroll down + */ + onScrollDown() { + if ( this.hasNextPage ) { + this.populateCollectionList(this.currentQuery, ++this.currentPage); + } + } + + /** + * Emit a [selectionChange] event when a new collection is selected from list + * + * @param event + * the selected [CollectionListEntry] + */ + onSelect(event: CollectionListEntry) { + this.selectionChange.emit(event); + } + + /** + * Method called for populate the collection list + * @param query text for filter the collection list + * @param page page number + */ + populateCollectionList(query: string, page: number) { + this.isLoadingList.next(true); + // Set the pagination info + const findOptions: FindListOptions = { + elementsPerPage: 10, + currentPage: page + }; + this.searchListCollection$ = this.collectionDataService + .getAuthorizedCollection(query, findOptions, followLink('parentCommunity')) + .pipe( + getSucceededRemoteWithNotEmptyData(), + switchMap((collections: RemoteData>) => { + if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) { + this.hasNextPage = false; + } + return collections.payload.page; + }), + mergeMap((collection: Collection) => collection.parentCommunity.pipe( + getFirstSucceededRemoteDataPayload(), + map((community: Community) => ({ + communities: [{ id: community.id, name: community.name }], + collection: { id: collection.id, uuid: collection.id, name: collection.name } + }) + ))), + reduce((acc: any, value: any) => [...acc, ...value], []), + startWith([]) + ); + this.subs.push(this.searchListCollection$.subscribe( + (next) => { this.searchListCollection.push(...next); }, undefined, + () => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); } + )); + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + /** + * Reset search form control + */ + reset() { + this.searchField.setValue(''); + } + + /** + * Reset pagination values + */ + resetPagination() { + this.currentPage = 1; + this.currentQuery = ''; + this.hasNextPage = true; + this.searchListCollection = []; + } + + /** + * Hide/Show the collection list loader + * @param hideShow true for show, false otherwise + */ + hideShowLoader(hideShow: boolean) { + this.isLoadingList.next(hideShow); + } +} diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index 454a036b15..3fcdc280d0 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -17,11 +17,12 @@ import { RestRequestMethod } from '../../../core/data/rest-request-method'; import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { hasValue } from '../../empty.util'; -import { AuthServiceMock } from '../../mocks/mock-auth.service'; +import { AuthServiceMock } from '../../mocks/auth.service.mock'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { VarDirective } from '../../utils/var.directive'; import { ComColFormComponent } from './comcol-form.component'; +import { Operation } from 'fast-json-patch'; describe('ComColFormComponent', () => { let comp: ComColFormComponent; @@ -40,11 +41,8 @@ describe('ComColFormComponent', () => { } }; const dcTitle = 'dc.title'; - const dcRandom = 'dc.random'; const dcAbstract = 'dc.description.abstract'; - const titleMD = { [dcTitle]: [{ value: 'Community Title', language: null }] }; - const randomMD = { [dcRandom]: [{ value: 'Random metadata excluded from form', language: null }] }; const abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] }; const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] }; const formModel = [ @@ -112,33 +110,47 @@ describe('ComColFormComponent', () => { }); it('should emit the new version of the community', () => { - comp.dso = Object.assign( - new Community(), - { - metadata: { - ...titleMD, - ...randomMD - } - } - ); + comp.dso = new Community(); comp.onSubmit(); + const operations: Operation[] = [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'New Community Title', + language: null, + }, + }, + { + op: 'replace', + path: '/metadata/dc.description.abstract', + value: { + value: 'Community description', + language: null, + }, + }, + ]; + expect(comp.submitForm.emit).toHaveBeenCalledWith( { - dso: Object.assign( - {}, - new Community(), - { + dso: Object.assign({}, comp.dso, { metadata: { - ...newTitleMD, - ...randomMD, - ...abstractMD + 'dc.title': [{ + value: 'New Community Title', + language: null, + }], + 'dc.description.abstract': [{ + value: 'Community description', + language: null, + }], }, - type: Community.type - }, + type: Community.type, + } ), uploader: undefined, - deleteLogo: false + deleteLogo: false, + operations: operations, } ); }) @@ -164,11 +176,6 @@ describe('ComColFormComponent', () => { it('should emit finish', () => { expect(comp.finish.emit).toHaveBeenCalled(); }); - - it('should remove the object\'s cache', () => { - expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); - expect(objectCacheStub.remove).toHaveBeenCalled(); - }); }); describe('onUploadError', () => { @@ -239,6 +246,11 @@ describe('ComColFormComponent', () => { it('should display a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); + + it('should remove the object\'s cache', () => { + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); + expect(objectCacheStub.remove).toHaveBeenCalled(); + }); }); describe('when dsoService.deleteLogo returns an error response', () => { @@ -318,7 +330,6 @@ describe('ComColFormComponent', () => { (comp as any).type = Community.type; comp.uploaderComponent = {uploader: {}} as any; - console.log(comp); (comp as any).dsoService = dsoService; fixture.detectChanges(); location = (comp as any).location; diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index f8199d2aad..91e896ce6c 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -25,6 +25,7 @@ import { hasValue, isNotEmpty } from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; import { UploaderOptions } from '../../uploader/uploader-options.model'; import { UploaderComponent } from '../../uploader/uploader.component'; +import { Operation } from 'fast-json-patch'; /** * A form for creating and editing Communities or Collections @@ -85,7 +86,8 @@ export class ComColFormComponent implements OnInit, OnDe @Output() submitForm: EventEmitter<{ dso: T, uploader: FileUploader, - deleteLogo: boolean + deleteLogo: boolean, + operations: Operation[], }> = new EventEmitter(); /** @@ -189,9 +191,9 @@ export class ComColFormComponent implements OnInit, OnDe const formMetadata = {} as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { const value: MetadataValue = { - value: fieldModel.value as string, - language: null - } as any; + value: fieldModel.value as string, + language: null + } as any; if (formMetadata.hasOwnProperty(fieldModel.name)) { formMetadata[fieldModel.name].push(value); } else { @@ -206,10 +208,26 @@ export class ComColFormComponent implements OnInit, OnDe }, type: Community.type }); + + const operations: Operation[] = []; + this.formModel.forEach((fieldModel: DynamicInputModel) => { + if (fieldModel.value !== this.dso.firstMetadataValue(fieldModel.name)) { + operations.push({ + op: 'replace', + path: `/metadata/${fieldModel.name}`, + value: { + value: fieldModel.value, + language: null, + }, + }); + } + }); + this.submitForm.emit({ dso: updatedDSO, uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, - deleteLogo: this.markLogoForDeletion + deleteLogo: this.markLogoForDeletion, + operations: operations, }); } @@ -257,7 +275,9 @@ export class ComColFormComponent implements OnInit, OnDe * The request was successful, display a success notification */ public onCompleteItem() { - this.refreshCache(); + if (hasValue(this.dso.id)) { + this.refreshCache(); + } this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); this.finish.emit(); } diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 717979891f..780589d0c5 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -14,10 +14,10 @@ import { CreateComColPageComponent } from './create-comcol-page.component'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ -} from '../../testing/utils'; +} from '../../remote-data.utils'; import { ComColDataService } from '../../../core/data/comcol-data.service'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; describe('CreateComColPageComponent', () => { let comp: CreateComColPageComponent; diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index e9373aff47..4a7cd9afb1 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -13,6 +13,7 @@ import { getSucceededRemoteData } from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; /** * Component representing the create page for communities and collections @@ -76,7 +77,8 @@ export class CreateComColPageComponent implements const uploader = event.uploader; this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.dsoDataService.create(dso, uuid) + const params = uuid ? [new RequestParam('parent', uuid)] : []; + this.dsoDataService.create(dso, ...params) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) { diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts index dbbeea5bc6..a3f6ac0216 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -12,7 +12,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DataService } from '../../../core/data/data.service'; import { DeleteComColPageComponent } from './delete-comcol-page.component'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; describe('DeleteComColPageComponent', () => { let comp: DeleteComColPageComponent; diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index 84454c4250..b50a1d3ac4 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -12,14 +12,14 @@ import { Community } from '../../../../core/shared/community.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NotificationsService } from '../../../notifications/notifications.service'; import { SharedModule } from '../../../shared.module'; -import { NotificationsServiceStub } from '../../../testing/notifications-service-stub'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../testing/utils'; +import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ComcolMetadataComponent } from './comcol-metadata.component'; describe('ComColMetadataComponent', () => { let comp: ComcolMetadataComponent; let fixture: ComponentFixture>; - let dsoDataService: CommunityDataService; + let dsoDataService; let router: Router; let community; @@ -49,6 +49,7 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), + patch: () => null, getLogoEndpoint: () => observableOf(logoEndpoint) }; @@ -95,37 +96,60 @@ describe('ComColMetadataComponent', () => { describe('with an empty queue in the uploader', () => { beforeEach(() => { data = { - dso: Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }), + operations: [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'test', + language: null, + }, + }, + ], + dso: new Community(), uploader: { options: { url: '' }, queue: [], /* tslint:disable:no-empty */ - uploadAll: () => {} + uploadAll: () => { + } /* tslint:enable:no-empty */ - } - } + }, + deleteLogo: false, + }; + spyOn(router, 'navigate'); }); - it('should navigate when successful', () => { - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); + describe('when successful', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: true, + })); + }); + + it('should navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); }); - it('should not navigate on failure', () => { - spyOn(router, 'navigate'); - spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity)); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + describe('on failure', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: false, + })); + }); + + it('should not navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts index 1031fead10..02c28d989c 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts @@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { ActivatedRoute, Router } from '@angular/router'; import { first, map, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; -import { hasValue, isNotUndefined } from '../../../empty.util'; -import { DataService } from '../../../../core/data/data.service'; +import { hasValue, isEmpty } from '../../../empty.util'; import { ResourceType } from '../../../../core/shared/resource-type'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { NotificationsService } from '../../../notifications/notifications.service'; @@ -49,26 +48,33 @@ export class ComcolMetadataComponent implements On * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ onSubmit(event) { - const dso = event.dso; + const uploader = event.uploader; const deleteLogo = event.deleteLogo; - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - if (hasValue(uploader) && uploader.queue.length > 0) { - this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); - } else if (!deleteLogo) { - this.router.navigate([this.frontendURL + newUUID]); - } - this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success')); - } + const newLogo = hasValue(uploader) && uploader.queue.length > 0; + if (newLogo) { + this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); }); + } + + if (!isEmpty(event.operations)) { + this.dsoDataService.patch(event.dso, event.operations) + .subscribe(async (response) => { + if (response.isSuccessful) { + if (!newLogo && !deleteLogo) { + await this.router.navigate([this.frontendURL + event.dso.uuid]); + } + this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`)); + } else if (response.statusCode === 403) { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`)); + } else { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`)); + } + }); + } } /** diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 091e02723f..477af5c1e4 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -1,27 +1,18 @@ -import { - ChangeDetectionStrategy, - Component, - Inject, - Input, NgZone, - OnDestroy, - OnInit -} from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; -import { Subscription } from 'rxjs/internal/Subscription'; -import { filter, map, startWith, tap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module'; import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { Router, ActivatedRoute, RouterModule, UrlSegment, Params } from '@angular/router'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; -import { hasValue } from '../empty.util'; +import { environment } from '../../../environments/environment'; export interface ComColPageNavOption { id: string; - label: string, - routerLink: string + label: string; + routerLink: string; params?: any; -}; +} /** * A component to display the "Browse By" section of a Community or Collection page @@ -48,13 +39,12 @@ export class ComcolPageBrowseByComponent implements OnInit { currentOptionId$: Observable; constructor( - @Inject(GLOBAL_CONFIG) public config: GlobalConfig, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { - this.allOptions = this.config.browseBy.types + this.allOptions = environment.browseBy.types .map((config: BrowseByTypeConfig) => ({ id: config.id, label: `browse.comcol.by.${config.id}`, diff --git a/src/app/shared/comcol-page-handle/comcol-page-handle.component.ts b/src/app/shared/comcol-page-handle/comcol-page-handle.component.ts index 3a2ab307be..bf403e9e88 100644 --- a/src/app/shared/comcol-page-handle/comcol-page-handle.component.ts +++ b/src/app/shared/comcol-page-handle/comcol-page-handle.component.ts @@ -1,7 +1,6 @@ -import { Component, Input, Inject, Injectable } from '@angular/core'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; +import { Component, Injectable, Input } from '@angular/core'; import { UIURLCombiner } from '../../core/url-combiner/ui-url-combiner'; + /** * This component builds a URL from the value of "handle" */ @@ -21,9 +20,7 @@ export class ComcolPageHandleComponent { // The value of "handle" @Input() content: string; - constructor(@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { - } public getHandle(): string { - return new UIURLCombiner(this.EnvConfig, '/handle/', this.content).toString(); + return new UIURLCombiner('/handle/', this.content).toString(); } } diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 5e91871967..afbdabc856 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -3,6 +3,8 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { isObject } from 'lodash'; import * as moment from 'moment'; +import { isNull } from './empty.util'; + /** * Returns true if the passed value is a NgbDateStruct. * @@ -13,7 +15,7 @@ import * as moment from 'moment'; */ export function isNgbDateStruct(value: object): boolean { return isObject(value) && value.hasOwnProperty('day') - && value.hasOwnProperty('month') && value.hasOwnProperty('year'); + && value.hasOwnProperty('month') && value.hasOwnProperty('year'); } /** @@ -56,3 +58,57 @@ export function dateToISOFormat(date: Date | NgbDateStruct): string { export function ngbDateStructToDate(date: NgbDateStruct): Date { return new Date(date.year, (date.month - 1), date.day); } + +/** + * Returns a NgbDateStruct object started from a string representing a date + * + * @param date + * The Date to convert + * @return NgbDateStruct + * the NgbDateStruct object + */ +export function stringToNgbDateStruct(date: string): NgbDateStruct { + return dateToNgbDateStruct(new Date(date)); +} + +/** + * Returns a NgbDateStruct object started from a Date object + * + * @param date + * The Date to convert + * @return NgbDateStruct + * the NgbDateStruct object + */ +export function dateToNgbDateStruct(date?: Date): NgbDateStruct { + if (isNull(date)) { + date = new Date() + } + + return { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate() + }; +} + +/** + * Returns a date in simplified format (YYYY-MM-DD). + * + * @param date + * The date to format + * @return string + * the formatted date + */ +export function dateToString(date: Date | NgbDateStruct): string { + const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); + + let year = dateObj.getFullYear().toString(); + let month = (dateObj.getMonth() + 1).toString(); + let day = dateObj.getDate().toString(); + + year = (year.length === 1) ? '0' + year : year; + month = (month.length === 1) ? '0' + month : month; + day = (day.length === 1) ? '0' + day : day; + const dateStr = `${year}-${month}-${day}`; + return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD'); +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 2de59d614b..b177e12988 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -8,7 +8,7 @@ import { ItemSearchResult } from '../../object-collection/shared/item-search-res import { Item } from '../../../core/shared/item.model'; import { PaginatedList } from '../../../core/data/paginated-list'; import { MetadataValue } from '../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject$ } from '../../testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; describe('DSOSelectorComponent', () => { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts index 97957d5250..df62534593 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts @@ -5,12 +5,12 @@ 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 { 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'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('CreateCollectionParentSelectorComponent', () => { let component: CreateCollectionParentSelectorComponent; @@ -39,7 +39,15 @@ describe('CreateCollectionParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html index e6a0db3b62..d0ba9c7108 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html @@ -14,6 +14,6 @@
{{'dso-selector.create.community.sub-level' | translate}}
- +